Node npm This extension supports running npm scripts defined in the package.jsonfile and validating the installed modules against the dependencies defined in the package.json. NoticeThe validation is done by running npmand it is not run when the modules are managed by yarn.
Deploy a Node.js Express.js application to Azure App Service (on Linux or Windows) using the Visual Studio Code App Service extension. Deploy your Node.js app to Azure using Git and the Azure App Service extension. To accomplish this goal. The Visual Studio Code editor has built-in debugging support for the Node.js runtime and can debug JavaScript, TypeScript, and many other languages that are transpiled into JavaScript. Setting up a project for Node.js debugging is straightforward with VS Code providing appropriate launch configuration defaults and snippets. Visual Studio Code is able to detect that this is a Node.js project, and as a result, automatically downloaded the TypeScript typings file for Node.js from NPM. The typings file allows you to get autocompletion for other Node.js globals, such as Buffer and setTimeout, as well as all of the built-in modules such as fs and http.
Introduction
While working on a CTF-style challenge recently I was introduced to Node.js. Now I have dealt with it before, and have done my share of web development using JavaScript so I’m not stranger to its intricacies, but this was the first time I’ve dealt with it server-side.
This challenge, specifically, had an implementation of safe-eval version 0.3.0. If you’re familiar with it, you may know that this has a well-known vulnerability (CVE-2017-16088) which allows a sandbox escape. Now the point of this post wasn’t to talk about this challenge or the vulnerability, but rather how I ended up identifying the next step in the exploitation process using remote debugging with VS Code.
Getting Started
If you haven’t used Visual Studio Code, you really don’t know what you’re missing. I was hesitant about using another Microsoft product (muh telemetries!?) but those worries are mitigable (pretty sure that’s a real word) via the horse’s mouth here. This editor has become my go-to for everything. It easily beats TextEdit.app’s startup time (incredibly frustrating). There’s an impressive number of plugins available (including ones for Vim keybindings), and (as is the point of this post) has support for Node.js debugging built right in!
The first step is getting it installed. Head over to https://code.visualstudio.com/. They aren’t joking about the “Free. Built on open source. Runs everywhere.” bit, I have it installed on my Mac, Windows laptop, and various Linux VMs. Follow the instructions for your platform, install it, and launch it.
Look at that beautiful editor, marvel at it. Click around, get comfy, install your favorite language plugin and be amazed it can take advantage of your system’s interpeter or install one if you don’t already have one. You like Java? There’s a whole kit for you crazy people, too. Come back here when you’re ready to move on.
Modifying launch.json
If you attempt to do any debugging without first opening a file (what exactly are you expecting here?) you may end up seeing the following error.
If that’s the case… well, follow the instructions and use the File menu to open a folder. It doesn’t really matter which one at this point, but do know that the configuration file you edit below (launch.json
) is going to live under the project folder you select, in the .vscode/
folder.
If you’ve tried some local debugging already, you may have clicked here and there, tried to start debugging, and maybe run up against something similar to the following.
Nevermind me trying to debug a markdown document, there’s that “Open launch.json” button. If you’re not familiar with VS Code, that file is what contains the various definitions used for launching and/or debugging your projects. If you click that button, launch.json
will open in a new tab. If you don’t happen to encounter that button, click the debug button on the left (the little bug icon with the no circle on it), then click the gear icon next to the play and launch program selections near the top.
If it asks you which environment to choose, select Node.js and continue. You’ll then likely end up looking at something simliar to the following.
At its core, that is the list of configurations you can use to debug a Node.js project. Of course that’s just for local debugging, and isn’t very fancy. What we need to do is add a configuration for remote debugging. Modify the code to match what’s below and then I’ll explain some of it.
As you can see we added another node
-type configuration, set its request
type to attach, given it an arbitrary name “Attach to remote”, specified our localhost as the address of the process we want to debug, and set the port to attach to as 9229
. You may find yourself asking, “Self, how do I connect to something remote if I’m specifying localhost? That doesn’t sound very remote at all.” Right you are, my friend. We’re gonna tunnel this bad boy.
Tangent: SSH Tunnel
The default debugging configuration for Node.js is to listen only on the loopback interface on port 9229
. By binding to that address (127.0.0.1
) all remote connections will be refused. You can confirm this by launching node with --inspect
and observing the following.
You can further confirm this by inspecting the output of netstat -ant
(or the equivalent on your platform) and seeing that it specifically binds to 127.0.0.1:9229
as you can see in the following.
This is a good thing! Notice that ws://
protocol descriptor? That stands for WebSocket. WebSockets are increasingly how the active web content world tends to work, but more importantly, the traffic is sent in the clear. That’s right, there’s no native encryption here. Now WebSockets does have a mechanism for attempting to prevent the reading of arbitrary bytes on the wire, and for more information on that I suggest you turn to Google or, if you’re up for some light reading, RFC6455 which defines WebSockets. This feature doesn’t (please correct me if I’m wrong) protect a full on packet capture as the “keys” used to XOR the data in transit are also transmitted in the clear.
The best way to ensure we are as secure as we can be is to employ an SSH tunnel. What this allows us to do is open an SSH session to our remote system and then tunnel all of our traffic from our local system (where we’re running VS Code) to the remote system over a specified port. All traffic sent over this tunnel will be encrypted and we can do our debugging without (too much) worry of snoopers.
To initiate a local SSH tunnel, SSH of course needs to be running on our target, and then you can issue the following command:
ssh -L 9229:127.0.0.1:9229 maik@remote-node-host
Here’s a breakdown of the parameters above:
-L
tells ssh to initiate a local tunnel, where a local port will listen for local connections to tunnel traffic9292:
is the port number ssh will open locally127.0.0.1
is the destination host we want to tunnel to relative to the remote host (so localhost because that’s what the debugger is bound to):9292
is the destination port we want to tunnel to on the destination host abovemaik@remote-node-host
is the username@hostname you would normally use to initiate an ssh connection
Ultimately this allows our VS Code debugger to tunnel from its own localhost:9229
and be redirected to 127.0.0.1:9229
on the remote host, where that otherwise wouldn’t be possible, all while being fully encrypted.
Debugging safe-eval
Now back to our regularly scheduled program. The following commands set up a quick environment for me to remotely test and debug safe-eval@0.3.0
.
Create a folder and cd
into it.
mkdir debugging && cd $_
Install safe-eval
version 0.3.0.
npm install safe-eval@0.3.0
Visual Studio Code Nodejs Extension
Create a test file to debug with. I made index.js with the following contents.
Invoke Node.js and enable debugging, but have it break immediately for the debugger.
node --inspect-brk index.js
Initiate the SSH tunnel:
ssh -L 9229:127.0.0.1:9229 maik@kali
Attach your local VS Code to the remote debugger by selecting the configuration we created earlier and then click the play button to begin debugging.
If all goes well we should see the remote Node.js console report a debugger was attached.
And then VS Code will show us just how amazing these features are!
Look, there’s our code right there! And on the left we can see the local variable scope!! We even have debugging controls in the toolbar at the top, and a handy dandy debug console at the bottom!!!
You can step over or into code. See how it shows us the call stack and loaded scripts? Wanna see what happens when we step intosafeEval
as it’s running? Try it! Hit the step over button (right arrow) three times, then the step into button (down arrow) once.
Now we’re on the first line of the implementation of safeEval
! We can see on the left our local scope has channged and we have access to the values passed in (code
as a string literal, context
as an object, etc) as well as the local function variables.
Want to take it further? I know you do. Step over until line 13, and then step into the call to vm.runInNewContext()
. Step over again until line 296 and then step into the call to createScript()
. We’re almost to where we want to be in this rabbit hole. Step into the call to new Script()
.
Curious and curiouser… We land in the Script
class constructor. Script
is a sublcass of ContextifyScript
. If we step through the constructor far enough we end up in a try/catch
which calls super()
(the constructor of the super class of Script
- ContextifyScript
) with a bunch of parameters including the code we are attempting to run in code
and the context we passed in parsingContext
.
As we can see there, parsingContext
shows us we have access to our bob
object, which is what we should expect. What’s fun, though, is because bob
was created outside the vm
instance, we could use that to climb back out of the sandbox as well. Fortunately, the current context object itself is enough to climb back out with.
Conclusion
Lots of fun to be had indeed! There’s one last thing I wanted to hit on that I’ve always loved about Visual Studio and now VS Code too. Intellisense! The challenge I was working on had some specific require
ments. I needed to figure out how to reference other node modules in the global scope after escaping the vm
sandbox.
As I was poking around in the debug console, I found a reference and was exploring the process
object just to see what I could see. Lo and behold, as I hit process.
the IntelliSense popup came up. I saw that process.mainModule.children
contained some other modules that I might be able to explore, but as I backspaced a bit to see what else was available, what should grace my eyes, but a reference to require
!! I was then able to slightly modify my exploit code and add the needed reference to require
and then pop the reverse shell.
Learn the steps to developer and debug your JavaScript Node.js project with Visual Studio.
Prepare your environment
Install Visual Studio Code.
Install git. Visual Studio Code integrates with git to provide Source Control management in the Side Bar.
Get a mongoDB database connection string.
If you don't have a mongoDB database available, you can:
- Choose to run this local project in a multi-container configuration where one of the containers is a mongoDB database. Install the Docker and Remote - Containers extension to get a multi-container configure with one of the containers running a local mongoDB database.
- Choose to create an Azure Cosmos DB resource for a mongoDB database. Learn more with this tutorial.
Clone sample project to local computer
To get started, download the sample project using the following steps:
Open Visual Studio Code.
Press F1 to display the command palette.
At the command palette prompt, enter
gitcl
, select the Git: Clone command, and press Enter.When prompted for the Repository URL, enter
https://github.com/scotch-io/node-todo
, then press Enter.Select (or create) the local directory into which you want to clone the project.
Use the integrated bash terminal to install dependencies
With this Node.js project, you must first ensure that all of the project's dependencies are installed from npm.
Press Ctrl+` to display the Visual Studio Code integrated terminal.
Enter
yarn
, and press Enter.
Visual Studio Code Node Modules
Navigate the project files and code
In order to orient ourselves within the codebase, let's play around with some examples of some of the navigation capabilities that Visual Studio Code provides.
Press Ctrl+P.
Enter
.js
to display all the JavaScript/JSON files in the project along with each file's parent directorySelect server.js, which is the startup script for the app.
Hover your mouse over the database variable (imported on line 6) to see its type. This ability to quickly inspect variables/modules/types within a file is useful during the development of your projects.
Clicking your mouse within the span of a variable - such as database - allows you to see all references to that variable within the same file. To view all references to a variable within the project, right-click the variable, and from the context menu, and select Find All References.
In addition to being to hover your mouse over a variable to discover its type, you can also inspect the definition of a variable, even if it's in another file. For example, right-click database.localUrl (line 12), and, from the context menu, select Peek Definition.
Use Visual Studio Code autocompletion with mongoDB
The MongoDB connection string is hard-coded in declaration of the database.localUrl
property. In this section, you'll modify the code to retrieve the connection string from an environment variable, and learn about Visual Studio Code's autocompletion feature.
Open the server.js file
Replace the following code:
with this code:
If you type the code in manually (instead of copy and paste), when you type the period after process
, Visual Studio Code displays the available members of the Node.js process global API.
Autocompletion works because Visual Studio Code uses TypeScript behind the scenes - even for JavaScript - to provide type information that can then be used to inform the completion list as you type. Visual Studio Code is able to detect that this is a Node.js project, and as a result, automatically downloaded the TypeScript typings file for Node.js from NPM. The typings file allows you to get autocompletion for other Node.js globals, such as Buffer
and setTimeout
, as well as all of the built-in modules such as fs
and http
.
In addition to the built-in Node.js APIs, this auto-acquisition of typings also works for over 2,000 third-party modules, such as React, Underscore, and Express. For example, in order to disable Mongoose from crashing the sample app if it can't connect to the configured MongoDB database instance, insert the following line of code at line 13:
As with the previous code, you'll notice that you get autocompletion without any work on your part.
You can see which modules support this autocomplete capability by browsing the DefinitelyTyped project, which is the community-driven source of all TypeScript type definitions.
Running the local Node.js app
Once you've explored the code a bit, it's time to run the app. To run the app from Visual Studio Code, press F5. When running the code via F5 (debug mode), Visual Studio Code launches the app and displays the Debug Console window that displays stdout for the app.
Additionally, the Debug Console is attached to the newly running app so you can type JavaScript expressions, which will be evaluated in the app, and also includes autocompletion. To see this behavior, type process.env
in the console:
You were able to press F5 to run the app because the currently open file is a JavaScript file (server.js). As a result, Visual Studio Code assumes that the project is a Node.js app. If you close all JavaScript files in Visual Studio Code, and then press F5, Visual Studio Code will query you as the environment:
Open a browser, and navigate to http://localhost:8080
to see the running app. Type a message into the textbox and add/remove a few to-dos's to get a feel for how the app works.
Debugging the local Node.js app
In addition to being able to run the app and interact with it via the integrated console, you can set breakpoints directly within your code. For example, press Ctrl+P to display the file picker. Once the file picker displays, type route
, and select the route.js file.
Set a breakpoint on line 28, which represents the Express route that is called when the app tries to add a to-do entry. To set a breakpoint, simply click the area to the left of the line number within the editor as shown in the following figure.
Note
In addition to standard breakpoints, Visual Studio Code supports conditional breakpoints that allow you to customize when the app should suspend execution. To set a conditional breakpoint, right-click the area to the left of the line on which you wish to pause execution, select Add Conditional Breakpoint, and specify either a JavaScript expression (for example, foo = 'bar'
) or execution count that defines the condition under which you want to pause execution.
Once the breakpoint has been set, return to the running app and add a to-do entry. Adding a to-do entry immediately causes the app to suspend execution on line 28 where you set the breakpoint:
Once the application has been paused, you can hover your mouse over the code's expressions to view their current value, inspect the locals/watches and call stack, and use the debug toolbar to step through the code execution. Press F5 to resume execution of the app.
Local full-stack debugging in Visual Studio Code
As mentioned earlier in the topic, the to-do app is a MEAN app - meaning that it's front-end and back-end are both written using JavaScript. So, while you're currently debugging the back-end (Node/Express) code, at some point, you may need to debug the front-end (Angular) code. For that purpose, Visual Studio Code has a huge ecosystem of extensions, including integrated Chrome debugging.
Switch to the Extensions tab, and type chrome
into the search box:
Select the extension named Debugger for Chrome, and select Install. After installing the Chrome debugging extension, select Reload to close and reopen Visual Studio Code in order to activate the extension.
While you were able to run and debug the Node.js code without any Visual Studio Code-specific configuration, in order to debug a front-end web app, you need to generate a launch.json file that instructs Visual Studio Code how to run the app.
Create a full-stack launch.json file for Visual Studio Code
To generate the launch.json file, switch to the Debug tab, select the gear icon (which should have a little red dot on top of it), and select the node.js environment.
Once created, the launch.json file looks similar to the following, and tells Visual Studio Code how to launch and/or attach to the app in order to debug it.
Visual Studio Code was able to detect that the app's startup script is server.js.
With the launch.json file open, select Add Configuration (bottom right), and select Chrome: Launch with userDataDir.
Adding a new run configuration for Chrome allows you to debug the front-end JavaScript code.
You can hover your mouse over any of the settings that are specified to view documentation about what the setting does. Additionally, notice that Visual Studio Code automatically detects the URL of the app. Update the webRoot property to ${workspaceRoot}/public
so that the Chrome debugger will know where to find the app's front-end assets:
In order to launch and debug both the front and back-end at the same time, you need to create a compound run configuration, which tells Visual Studio Code which set of configurations to run in parallel.
Add the following snippet as a top-level property within the launch.json file (as a sibling of the existing configurations property).
The string values specified in the compounds.configurations array refer to the name of individual entries in the list of configurations. If you've modified those names, you'll need to make the appropriate changes in the array. For example, switch to the debug tab, and change the selected configuration to Full-Stack (the name of the compound configuration), and press F5 to run it.
Running the configuration launches the Node.js app (as can be seen in the debug console output) and Chrome (configured to navigate to the Node.js app at http://localhost:8080
).
Press Ctrl+P, and enter (or select) todos.js, which is the main Angular controller for the app's front end.
Set a breakpoint on line 11, which is the entry-point for a new to-do entry being created.
Return to the running app, add a new to-do entry, and notice that Visual Studio Code has now suspended execution within the Angular code.
Visual Studio Code Node Js Install
Like Node.js debugging, you can hover your mouse over expressions, view locals/watches, evaluate expressions in the console, and so on.
There are two cools things to note:
The Call Stack pane displays two different stacks: Node and Chrome, and indicates which one is currently paused.
You can step between front and back-end code: press F5, which will run and hit the breakpoint previously set in the Express route.
With this setup, you can now efficiently debug front, back, or full-stack JavaScript code directly within Visual Studio Code.
In addition, the compound debugger concept is not limited to just two target processes, and also isn't just limited to JavaScript. Therefore, if work on a microservice app (that is potentially polyglot), you can use the exact same workflow (once you've installed the appropriate extensions for the language/framework).